diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 8c9d343443..a40f470aa0 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -3,7 +3,6 @@
-
diff --git a/app/src/main/java/com/infomaniak/mail/MatomoMail.kt b/app/src/main/java/com/infomaniak/mail/MatomoMail.kt
index 279990aebe..54f5af12fc 100644
--- a/app/src/main/java/com/infomaniak/mail/MatomoMail.kt
+++ b/app/src/main/java/com/infomaniak/mail/MatomoMail.kt
@@ -168,6 +168,10 @@ object MatomoMail : MatomoCore {
}
fun Fragment.trackCreateFolderEvent(name: String) {
+ context?.trackCreateFolderEvent(name)
+ }
+
+ fun Context.trackCreateFolderEvent(name: String) {
trackEvent("createFolder", name)
}
@@ -209,10 +213,6 @@ object MatomoMail : MatomoCore {
trackEvent("invalidPasswordMailbox", name)
}
- fun Fragment.trackExternalEvent(name: String, action: TrackerAction = TrackerAction.CLICK, value: Float? = null) {
- context?.trackExternalEvent(name, action, value)
- }
-
fun Context.trackExternalEvent(name: String, action: TrackerAction = TrackerAction.CLICK, value: Float? = null) {
trackEvent("externals", name, action, value)
}
diff --git a/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt b/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt
index 8943b4ee95..688b09b5fa 100644
--- a/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt
+++ b/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt
@@ -20,7 +20,11 @@ package com.infomaniak.mail.data.api
import com.infomaniak.lib.core.InfomaniakCore
import com.infomaniak.lib.core.R
import com.infomaniak.lib.core.api.ApiController
-import com.infomaniak.lib.core.api.ApiController.ApiMethod.*
+import com.infomaniak.lib.core.api.ApiController.ApiMethod.DELETE
+import com.infomaniak.lib.core.api.ApiController.ApiMethod.GET
+import com.infomaniak.lib.core.api.ApiController.ApiMethod.PATCH
+import com.infomaniak.lib.core.api.ApiController.ApiMethod.POST
+import com.infomaniak.lib.core.api.ApiController.ApiMethod.PUT
import com.infomaniak.lib.core.api.ApiRepositoryCore
import com.infomaniak.lib.core.models.ApiResponse
import com.infomaniak.lib.core.models.ApiResponseStatus
diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt
index f94ee6fed9..648e730f36 100644
--- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt
+++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt
@@ -25,6 +25,7 @@ import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.utils.extensions.copyListToRealm
import com.infomaniak.mail.utils.extensions.flattenFolderChildren
+import com.infomaniak.mail.utils.extensions.sortFolders
import io.realm.kotlin.MutableRealm
import io.realm.kotlin.Realm
import io.realm.kotlin.TypedRealm
@@ -34,7 +35,6 @@ import io.realm.kotlin.notifications.ResultsChange
import io.realm.kotlin.query.RealmQuery
import io.realm.kotlin.query.RealmResults
import io.realm.kotlin.query.RealmSingleQuery
-import io.realm.kotlin.query.Sort
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapNotNull
import javax.inject.Inject
@@ -45,20 +45,20 @@ class FolderController @Inject constructor(
) {
//region Get data
- fun getCustomFolders(): RealmResults {
- return getCustomFoldersQuery(mailboxContentRealm()).find()
+ fun getMenuDrawerDefaultFoldersAsync(): Flow> {
+ return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.CUSTOM, withoutChildren = true).asFlow()
}
- fun getRootsFoldersAsync(): Flow> {
- return getFoldersQuery(mailboxContentRealm(), onlyRoots = true).asFlow()
+ fun getMenuDrawerCustomFoldersAsync(): Flow> {
+ return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.DEFAULT, withoutChildren = true).asFlow()
}
- fun getDefaultFoldersAsync(): Flow> {
- return getDefaultFoldersQuery(mailboxContentRealm()).asFlow()
+ fun getSearchFoldersAsync(): Flow> {
+ return getFoldersQuery(mailboxContentRealm(), withoutChildren = true).asFlow()
}
- fun getCustomFoldersAsync(): Flow> {
- return getCustomFoldersQuery(mailboxContentRealm()).asFlow()
+ fun getMoveFolders(): RealmResults {
+ return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.DRAFT, withoutChildren = true).find()
}
fun getFolder(id: String): Folder? {
@@ -131,31 +131,31 @@ class FolderController @Inject constructor(
}
//endregion
+ enum class FoldersType {
+ DEFAULT,
+ CUSTOM,
+ DRAFT,
+ }
+
companion object {
const val SEARCH_FOLDER_ID = "search_folder_id"
private val isNotSearch = "${Folder::id.name} != '$SEARCH_FOLDER_ID'"
private val isRootFolder = "${Folder.parentsPropertyName}.@count == 0"
//region Queries
- private fun getFoldersQuery(realm: TypedRealm, onlyRoots: Boolean = false): RealmQuery {
- val rootsQuery = if (onlyRoots) " AND $isRootFolder" else ""
- return realm
- .query(isNotSearch + rootsQuery)
- .sort(Folder::name.name, Sort.ASCENDING)
- .sort(Folder::isFavorite.name, Sort.DESCENDING)
- }
-
- private fun getDefaultFoldersQuery(realm: TypedRealm): RealmQuery {
- val hasRole = "${Folder.rolePropertyName} != nil"
- return realm.query("$isNotSearch AND $hasRole")
- }
-
- private fun getCustomFoldersQuery(realm: TypedRealm): RealmQuery {
- val hasNoRole = "${Folder.rolePropertyName} == nil"
- return realm
- .query("$isNotSearch AND $isRootFolder AND $hasNoRole")
- .sort(Folder::name.name, Sort.ASCENDING)
- .sort(Folder::isFavorite.name, Sort.DESCENDING)
+ private fun getFoldersQuery(
+ realm: TypedRealm,
+ withoutType: FoldersType? = null,
+ withoutChildren: Boolean = false,
+ ): RealmQuery {
+ val rootsQuery = if (withoutChildren) " AND $isRootFolder" else ""
+ val typeQuery = when (withoutType) {
+ FoldersType.DEFAULT -> " AND ${Folder.rolePropertyName} == nil"
+ FoldersType.CUSTOM -> " AND ${Folder.rolePropertyName} != nil"
+ FoldersType.DRAFT -> " AND ${Folder.rolePropertyName} != '${FolderRole.DRAFT.name}'"
+ null -> ""
+ }
+ return realm.query("$isNotSearch${rootsQuery}${typeQuery}").sortFolders()
}
private fun getFoldersQuery(exceptionsFoldersIds: List, realm: TypedRealm): RealmQuery {
diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt
index de90b8d643..70d44d5e5e 100644
--- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt
+++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt
@@ -22,7 +22,9 @@ import com.infomaniak.lib.core.utils.SentryLog
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.LocalSettings.ThreadMode
import com.infomaniak.mail.data.api.ApiRepository
-import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode.*
+import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode.ONE_PAGE_OF_OLD_MESSAGES
+import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode.REFRESH_FOLDER
+import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode.REFRESH_FOLDER_WITH_ROLE
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RetryStrategy.Iteration
import com.infomaniak.mail.data.cache.mailboxInfo.MailboxController
import com.infomaniak.mail.data.models.Folder
diff --git a/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt b/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt
index c8a0cd83d4..aeebb9b2c3 100644
--- a/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt
+++ b/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt
@@ -23,6 +23,7 @@ import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.infomaniak.lib.core.utils.Utils.enumValueOfOrNull
+import com.infomaniak.lib.core.utils.removeAccents
import com.infomaniak.mail.R
import com.infomaniak.mail.data.models.message.Message
import com.infomaniak.mail.data.models.thread.Thread
@@ -43,7 +44,7 @@ import kotlinx.serialization.UseSerializers
import kotlin.math.max
@Serializable
-class Folder : RealmObject {
+class Folder : RealmObject, Cloneable {
//region Remote data
@PrimaryKey
@@ -79,6 +80,10 @@ class Folder : RealmObject {
var isHidden: Boolean = false // For children only (a children Folder is hidden if its parent is collapsed)
@Transient
var isCollapsed: Boolean = false // For parents only (collapsing a parent Folder will hide its children)
+ @Transient
+ var roleOrder: Int = role?.order ?: CUSTOM_FOLDER_ROLE_ORDER
+ @Transient
+ var sortedName: String = name
//endregion
private val _parents by backlinks(Folder::children)
@@ -99,6 +104,9 @@ class Folder : RealmObject {
val isRoot: Boolean
inline get() = !path.contains(separator)
+ val isRootAndCustom: Boolean
+ inline get() = role == null && isRoot
+
fun initLocalValues(
lastUpdatedAt: RealmInstant?,
cursor: String?,
@@ -120,6 +128,8 @@ class Folder : RealmObject {
this.isHistoryComplete = isHistoryComplete
this.isHidden = isHidden
this.isCollapsed = isCollapsed
+
+ this.sortedName = this.name.lowercase().removeAccents()
}
fun resetLocalValues() {
@@ -153,14 +163,14 @@ class Folder : RealmObject {
val order: Int,
val matomoValue: String,
) {
- INBOX(R.string.inboxFolder, R.drawable.ic_drawer_inbox, 0, "inboxFolder"),
+ INBOX(R.string.inboxFolder, R.drawable.ic_drawer_inbox, 8, "inboxFolder"),
+ COMMERCIAL(R.string.commercialFolder, R.drawable.ic_promotions, 7, "commercialFolder"),
+ SOCIALNETWORKS(R.string.socialNetworksFolder, R.drawable.ic_social_media, 6, "socialNetworksFolder"),
+ SENT(R.string.sentFolder, R.drawable.ic_sent_messages, 5, "sentFolder"),
DRAFT(R.string.draftFolder, R.drawable.ic_draft, 4, "draftFolder"),
- SENT(R.string.sentFolder, R.drawable.ic_sent_messages, 3, "sentFolder"),
- SPAM(R.string.spamFolder, R.drawable.ic_spam, 5, "spamFolder"),
- TRASH(R.string.trashFolder, R.drawable.ic_bin, 6, "trashFolder"),
- ARCHIVE(R.string.archiveFolder, R.drawable.ic_archive_folder, 7, "archiveFolder"),
- COMMERCIAL(R.string.commercialFolder, R.drawable.ic_promotions, 1, "commercialFolder"),
- SOCIALNETWORKS(R.string.socialNetworksFolder, R.drawable.ic_social_media, 2, "socialNetworksFolder"),
+ SPAM(R.string.spamFolder, R.drawable.ic_spam, 3, "spamFolder"),
+ TRASH(R.string.trashFolder, R.drawable.ic_bin, 2, "trashFolder"),
+ ARCHIVE(R.string.archiveFolder, R.drawable.ic_archive_folder, 1, "archiveFolder"),
}
companion object {
@@ -173,5 +183,6 @@ class Folder : RealmObject {
const val DEFAULT_IS_HISTORY_COMPLETE = false
const val INBOX_FOLDER_ID = "eJzz9HPyjwAABGYBgQ--"
+ private const val CUSTOM_FOLDER_ROLE_ORDER = 0
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/data/models/Quotas.kt b/app/src/main/java/com/infomaniak/mail/data/models/Quotas.kt
index fcb5dd4710..07a08ee67b 100644
--- a/app/src/main/java/com/infomaniak/mail/data/models/Quotas.kt
+++ b/app/src/main/java/com/infomaniak/mail/data/models/Quotas.kt
@@ -31,7 +31,7 @@ class Quotas : EmbeddedRealmObject {
@SerialName("size")
private var _size: Long = 0L
- private val size: Long get() = _size * 1_000L // Convert from KiloOctets to Octets
+ val size: Long get() = _size * 1_000L // Convert from KiloOctets to Octets
fun getText(context: Context): String {
diff --git a/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt b/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt
index 298dd88550..fc6d97bee6 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt
@@ -62,10 +62,9 @@ import com.infomaniak.mail.data.models.draft.Draft.DraftAction
import com.infomaniak.mail.databinding.ActivityMainBinding
import com.infomaniak.mail.firebase.RegisterFirebaseBroadcastReceiver
import com.infomaniak.mail.ui.alertDialogs.DescriptionAlertDialog
-import com.infomaniak.mail.ui.alertDialogs.TitleAlertDialog
import com.infomaniak.mail.ui.main.SnackbarManager
import com.infomaniak.mail.ui.main.folder.TwoPaneFragment
-import com.infomaniak.mail.ui.main.menu.MenuDrawerFragment
+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.newMessage.NewMessageActivity
@@ -144,9 +143,6 @@ class MainActivity : BaseActivity() {
@Inject
lateinit var descriptionDialog: DescriptionAlertDialog
- @Inject
- lateinit var titleDialog: TitleAlertDialog
-
@Inject
lateinit var permissionUtils: PermissionUtils
diff --git a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt
index 209ba9c1d8..201cebefd4 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt
@@ -69,8 +69,7 @@ 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.appContext
-import com.infomaniak.mail.utils.extensions.getCustomMenuFolders
-import com.infomaniak.mail.utils.extensions.getDefaultMenuFolders
+import com.infomaniak.mail.utils.extensions.flattenFolderChildren
import com.infomaniak.mail.utils.extensions.getFoldersIds
import com.infomaniak.mail.utils.extensions.getUids
import com.infomaniak.mail.utils.extensions.launchNoValidMailboxesActivity
@@ -85,6 +84,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
@@ -119,7 +119,6 @@ class MainViewModel @Inject constructor(
private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher)
private var refreshEverythingJob: Job? = null
- // First boolean is the download status, second boolean is if the LoadMore button should be displayed
val isDownloadingChanges: MutableLiveData = MutableLiveData(false)
val isInternetAvailable = MutableLiveData()
val isMovedToNewFolder = SingleLiveEvent()
@@ -158,14 +157,12 @@ class MainViewModel @Inject constructor(
it?.let(mailboxController::getMailbox)
}.asLiveData(ioCoroutineContext)
- val currentDefaultFoldersLive = _currentMailboxObjectId.flatMapLatest { objectId ->
- objectId?.let { folderController.getDefaultFoldersAsync().map { it.list.getDefaultMenuFolders() } } ?: emptyFlow()
+ val defaultFoldersLive = _currentMailboxObjectId.filterNotNull().flatMapLatest {
+ folderController.getMenuDrawerDefaultFoldersAsync().map { it.list.flattenFolderChildren(dismissHiddenChildren = true) }
}.asLiveData(ioCoroutineContext)
- val currentCustomFoldersLive = _currentMailboxObjectId.flatMapLatest { objectId ->
- objectId
- ?.let { folderController.getCustomFoldersAsync().map { it.list.getCustomMenuFolders(dismissHiddenChildren = true) } }
- ?: emptyFlow()
+ val customFoldersLive = _currentMailboxObjectId.filterNotNull().flatMapLatest {
+ folderController.getMenuDrawerCustomFoldersAsync().map { it.list.flattenFolderChildren(dismissHiddenChildren = true) }
}.asLiveData(ioCoroutineContext)
val currentQuotasLive = _currentMailboxObjectId.flatMapLatest {
diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/CreateFolderDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/CreateFolderDialog.kt
new file mode 100644
index 0000000000..3e596c14d1
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/CreateFolderDialog.kt
@@ -0,0 +1,54 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.alertDialogs
+
+import android.content.Context
+import androidx.annotation.StringRes
+import com.infomaniak.mail.MatomoMail.trackCreateFolderEvent
+import com.infomaniak.mail.R
+import com.infomaniak.mail.data.cache.mailboxContent.FolderController
+import com.infomaniak.mail.di.IoDispatcher
+import com.infomaniak.mail.utils.extensions.getFolderCreationError
+import dagger.hilt.android.qualifiers.ActivityContext
+import dagger.hilt.android.scopes.ActivityScoped
+import kotlinx.coroutines.CoroutineDispatcher
+import javax.inject.Inject
+
+@ActivityScoped
+class CreateFolderDialog @Inject constructor(
+ @ActivityContext private val activityContext: Context,
+ @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
+ private val folderController: FolderController,
+) : InputAlertDialog(activityContext, ioDispatcher) {
+
+ fun show(@StringRes confirmButtonText: Int = R.string.buttonCreate) = show(
+ title = R.string.newFolderDialogTitle,
+ hint = R.string.newFolderDialogHint,
+ confirmButtonText = confirmButtonText,
+ )
+
+ fun setCallbacks(onPositiveButtonClicked: (String) -> Unit) = setCallbacks(
+ onPositiveButtonClicked = { folderName ->
+ activityContext.trackCreateFolderEvent("confirm")
+ onPositiveButtonClicked(folderName)
+ },
+ onErrorCheck = { folderName ->
+ activityContext.getFolderCreationError(folderName, folderController)
+ },
+ )
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/InputAlertDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/InputAlertDialog.kt
index 24d5fe7363..57b61aa166 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/InputAlertDialog.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/InputAlertDialog.kt
@@ -42,7 +42,7 @@ import javax.inject.Inject
import com.infomaniak.lib.core.R as RCore
@ActivityScoped
-class InputAlertDialog @Inject constructor(
+open class InputAlertDialog @Inject constructor(
@ActivityContext private val activityContext: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : BaseAlertDialog(activityContext) {
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/MailboxListFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/MailboxListFragment.kt
index f25975329b..3601569c25 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/MailboxListFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/MailboxListFragment.kt
@@ -23,7 +23,7 @@ import com.infomaniak.lib.core.utils.safeNavigate
import com.infomaniak.mail.R
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.ui.bottomSheetDialogs.LockedMailboxBottomSheetDialogArgs
-import com.infomaniak.mail.ui.main.menu.MailboxesAdapter
+import com.infomaniak.mail.ui.main.menuDrawer.MailboxesAdapter
import com.infomaniak.mail.utils.AccountUtils
import kotlinx.coroutines.launch
@@ -49,7 +49,7 @@ interface MailboxListFragment {
)
}
- fun Fragment.onValidMailboxClicked(mailboxId: Int) {
- lifecycleScope.launch { AccountUtils.switchToMailbox(mailboxId) }
+ fun Fragment.onValidMailboxClicked(mailboxId: Int) = lifecycleScope.launch {
+ AccountUtils.switchToMailbox(mailboxId)
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/HeaderItemDecoration.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/HeaderItemDecoration.kt
index 2e461ce000..dc9d7d0ccf 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/HeaderItemDecoration.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/HeaderItemDecoration.kt
@@ -25,8 +25,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.forEachIndexed
import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.RecyclerView.*
-import com.infomaniak.mail.ui.main.folder.HeaderItemDecoration.Intersection.*
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
+import androidx.recyclerview.widget.RecyclerView.ItemDecoration
+import androidx.recyclerview.widget.RecyclerView.NO_POSITION
+import androidx.recyclerview.widget.RecyclerView.SimpleOnItemTouchListener
+import androidx.recyclerview.widget.RecyclerView.State
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.infomaniak.mail.ui.main.folder.HeaderItemDecoration.Intersection.CENTER
+import com.infomaniak.mail.ui.main.folder.HeaderItemDecoration.Intersection.INSET_BOTTOM
+import com.infomaniak.mail.ui.main.folder.HeaderItemDecoration.Intersection.INSET_TOP
class HeaderItemDecoration(
parent: RecyclerView,
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt
index a11343a0ab..ce7eb3a3e5 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt
@@ -113,7 +113,7 @@ class ThreadListAdapter @Inject constructor(
private var folderRole: FolderRole? = null
private var multiSelection: MultiSelectionListener? = null
private var isFolderNameVisible: Boolean = false
- private var threadListAdapterCallback: ThreadListAdapterCallback? = null
+ private var callbacks: ThreadListAdapterCallbacks? = null
private var previousThreadClickedPosition: Int? = null
@@ -130,14 +130,14 @@ class ThreadListAdapter @Inject constructor(
operator fun invoke(
folderRole: FolderRole?,
- threadListAdapterCallback: ThreadListAdapterCallback,
+ callbacks: ThreadListAdapterCallbacks,
multiSelection: MultiSelectionListener? = null,
isFolderNameVisible: Boolean = false,
) {
this.folderRole = folderRole
this.multiSelection = multiSelection
this.isFolderNameVisible = isFolderNameVisible
- this.threadListAdapterCallback = threadListAdapterCallback
+ this.callbacks = callbacks
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
@@ -218,7 +218,7 @@ class ThreadListAdapter @Inject constructor(
if (thread.messages.isEmpty()) {
// TODO: Find why we are sometimes displaying empty Threads, and fix it instead of just deleting them.
// It's possibly because we are out of sync, and the situation will resolve by itself shortly?
- threadListAdapterCallback?.deleteThreadInRealm?.invoke(thread.uid)
+ callbacks?.deleteThreadInRealm?.invoke(thread.uid)
SentryDebug.sendEmptyThread(thread, "No Message in the Thread when displaying it in ThreadList")
return
}
@@ -341,7 +341,7 @@ class ThreadListAdapter @Inject constructor(
if (multiSelection?.isEnabled == true) {
toggleMultiSelectedThread(thread)
} else {
- threadListAdapterCallback?.onThreadClicked?.invoke(thread)
+ callbacks?.onThreadClicked?.invoke(thread)
// If the Thread is `onlyOneDraft`, we'll directly navigate to the NewMessageActivity.
// It means that we won't go to the ThreadFragment, so there's no need to select anything.
if (thread.uid != openedThreadUid && !thread.isOnlyOneDraft) selectNewThread(position, thread.uid)
@@ -358,7 +358,7 @@ class ThreadListAdapter @Inject constructor(
if (oldPosition != null && oldPosition < itemCount) notifyItemChanged(oldPosition, NotificationType.SELECTED_STATE)
if (newPosition != null) {
previousThreadClickedPosition?.let {
- threadListAdapterCallback?.onPositionClickedChanged?.invoke(newPosition, it)
+ callbacks?.onPositionClickedChanged?.invoke(newPosition, it)
}
previousThreadClickedPosition = newPosition
notifyItemChanged(newPosition, NotificationType.SELECTED_STATE)
@@ -510,7 +510,7 @@ class ThreadListAdapter @Inject constructor(
val (hintTextId, buttonTextId) = when (folderRole) {
FolderRole.SPAM -> R.string.threadListSpamHint to R.string.threadListEmptySpamButton
FolderRole.TRASH -> R.string.threadListTrashHint to R.string.threadListEmptyTrashButton
- else -> throw IllegalStateException("We are trying to flush a non-flushable folder.")
+ else -> error("We are trying to flush a non-flushable folder.")
}
root.apply {
@@ -518,14 +518,14 @@ class ThreadListAdapter @Inject constructor(
val buttonText = context.getString(buttonTextId)
actionButtonText = buttonText
- setOnActionClickListener { threadListAdapterCallback?.onFlushClicked?.invoke(buttonText) }
+ setOnActionClickListener { callbacks?.onFlushClicked?.invoke(buttonText) }
}
}
private fun ItemThreadLoadMoreButtonBinding.displayLoadMoreButton() {
loadMoreButton.setOnClickListener {
if (dataSet.last() is Unit) dataSet = dataSet.toMutableList().apply { removeLastOrNull() }
- threadListAdapterCallback?.onLoadMoreClicked?.invoke()
+ callbacks?.onLoadMoreClicked?.invoke()
}
}
@@ -602,7 +602,7 @@ class ThreadListAdapter @Inject constructor(
override fun onSwipeAnimationFinished(viewHolder: ThreadListViewHolder) {
viewHolder.isSwipedOverHalf = false
- threadListAdapterCallback?.onSwipeFinished?.invoke()
+ callbacks?.onSwipeFinished?.invoke()
unblockOtherSwipes()
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapterCallback.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapterCallbacks.kt
similarity index 96%
rename from app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapterCallback.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapterCallbacks.kt
index e4dd9c5f7b..c012a49f1e 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapterCallback.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapterCallbacks.kt
@@ -19,7 +19,7 @@ package com.infomaniak.mail.ui.main.folder
import com.infomaniak.mail.data.models.thread.Thread
-interface ThreadListAdapterCallback {
+interface ThreadListAdapterCallbacks {
var onSwipeFinished: (() -> Unit)?
var onThreadClicked: (thread: Thread) -> Unit
var onFlushClicked: ((dialogTitle: String) -> Unit)?
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt
index fee51ff486..d69339f1b4 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt
@@ -304,7 +304,7 @@ class ThreadListFragment : TwoPaneFragment(), SwipeRefreshLayout.OnRefreshListen
private fun setupAdapter() {
threadListAdapter(
folderRole = mainViewModel.currentFolder.value?.role,
- threadListAdapterCallback = object : ThreadListAdapterCallback {
+ callbacks = object : ThreadListAdapterCallbacks {
override var onSwipeFinished: (() -> Unit)? = { threadListViewModel.isRecoveringFinished.value = true }
@@ -506,7 +506,7 @@ class ThreadListFragment : TwoPaneFragment(), SwipeRefreshLayout.OnRefreshListen
notYetImplemented()
true
}
- SwipeAction.NONE -> throw IllegalStateException("Cannot swipe on an action which is not set")
+ SwipeAction.NONE -> error("Cannot swipe on an action which is not set")
}
val shouldKeepItemBecauseOfNoConnection = isInternetAvailable.value == false
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/FolderAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menu/FolderAdapter.kt
deleted file mode 100644
index ea09564923..0000000000
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/FolderAdapter.kt
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Infomaniak Mail - Android
- * Copyright (C) 2022-2024 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package com.infomaniak.mail.ui.main.menu
-
-import android.annotation.SuppressLint
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.annotation.DrawableRes
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.recyclerview.widget.AsyncListDiffer
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.RecyclerView.Adapter
-import androidx.recyclerview.widget.RecyclerView.ViewHolder
-import androidx.viewbinding.ViewBinding
-import com.infomaniak.mail.MatomoMail.trackMenuDrawerEvent
-import com.infomaniak.mail.R
-import com.infomaniak.mail.data.models.Folder
-import com.infomaniak.mail.data.models.Folder.FolderRole
-import com.infomaniak.mail.databinding.ItemFolderMenuDrawerBinding
-import com.infomaniak.mail.databinding.ItemSelectableFolderBinding
-import com.infomaniak.mail.ui.main.menu.FolderAdapter.FolderViewHolder
-import com.infomaniak.mail.utils.UnreadDisplay
-import com.infomaniak.mail.utils.Utils.runCatchingRealm
-import com.infomaniak.mail.views.itemViews.SelectableFolderItemView
-import com.infomaniak.mail.views.itemViews.SelectableItemView
-import com.infomaniak.mail.views.itemViews.SelectableMailboxItemView
-import com.infomaniak.mail.views.itemViews.UnreadFolderItemView
-import com.infomaniak.mail.views.itemViews.UnreadItemView
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.invoke
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-import kotlin.math.min
-
-class FolderAdapter @Inject constructor(
- private val globalCoroutineScope: CoroutineScope,
-) : Adapter() {
-
- private val foldersDiffer = AsyncListDiffer(this, FolderDiffCallback())
-
- private inline val folders get() = foldersDiffer.currentList
-
- private var currentFolderId: String? = null
- private var hasCollapsableFolder: Boolean? = null
-
- private var isInMenuDrawer: Boolean = true
- private var shouldIndent: Boolean = true
- private lateinit var onFolderClicked: (folderId: String) -> Unit
- private var onCollapseClicked: ((folderId: String, shouldCollapse: Boolean) -> Unit)? = null
- private var onCollapseTransition: ((Boolean) -> Unit)? = null
-
- private var setFoldersJob: Job? = null
-
- operator fun invoke(
- isInMenuDrawer: Boolean,
- shouldIndent: Boolean = true,
- onFolderClicked: (folderId: String) -> Unit,
- onCollapseClicked: ((folderId: String, shouldCollapse: Boolean) -> Unit)? = null,
- onCollapseTransition: ((Boolean) -> Unit)? = null,
- ): FolderAdapter {
- this.isInMenuDrawer = isInMenuDrawer
- this.shouldIndent = shouldIndent
- this.onFolderClicked = onFolderClicked
- this.onCollapseClicked = onCollapseClicked
- this.onCollapseTransition = onCollapseTransition
-
- return this
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
- val layoutInflater = LayoutInflater.from(parent.context)
- val binding = if (viewType == DisplayType.SELECTABLE_FOLDER.layout) {
- ItemSelectableFolderBinding.inflate(layoutInflater, parent, false)
- } else {
- ItemFolderMenuDrawerBinding.inflate(layoutInflater, parent, false)
- }
-
- return FolderViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: FolderViewHolder, position: Int, payloads: MutableList) = runCatchingRealm {
- if (payloads.firstOrNull() == Unit) {
- val folder = folders[position]
- if (getItemViewType(position) == DisplayType.SELECTABLE_FOLDER.layout) {
- (holder.binding as ItemSelectableFolderBinding).root.setSelectedState(currentFolderId == folder.id)
- }
- } else {
- super.onBindViewHolder(holder, position, payloads)
- }
- }.getOrDefault(Unit)
-
- override fun onBindViewHolder(holder: FolderViewHolder, position: Int) = with(holder.binding) {
- val folder = folders[position]
-
- when (getItemViewType(position)) {
- DisplayType.SELECTABLE_FOLDER.layout -> (this as ItemSelectableFolderBinding).root.displayFolder(folder)
- DisplayType.MENU_DRAWER.layout -> (this as ItemFolderMenuDrawerBinding).root.displayMenuDrawerFolder(folder)
- }
- }
-
- override fun getItemViewType(position: Int): Int {
- return if (isInMenuDrawer) DisplayType.MENU_DRAWER.layout else DisplayType.SELECTABLE_FOLDER.layout
- }
-
- override fun getItemCount(): Int = runCatchingRealm { folders.size }.getOrDefault(0)
-
- private fun UnreadFolderItemView.displayMenuDrawerFolder(folder: Folder) {
-
- val unread = when (folder.role) {
- FolderRole.DRAFT -> UnreadDisplay(folder.threads.count())
- FolderRole.SENT, FolderRole.TRASH -> UnreadDisplay(0)
- else -> folder.unreadCountDisplay
- }
-
- displayFolder(folder, unread)
- }
-
- private fun SelectableItemView.displayFolder(folder: Folder, unread: UnreadDisplay? = null) {
- folder.role?.let {
- setFolderUi(folder, it.folderIconRes, unread, it.matomoValue)
- } ?: run {
- val indentLevel = if (shouldIndent) folder.path.split(folder.separator).size - 1 else 0
- setFolderUi(
- folder = folder,
- iconId = if (folder.isFavorite) R.drawable.ic_folder_star else R.drawable.ic_folder,
- unread = unread,
- trackerName = "customFolder",
- trackerValue = indentLevel.toFloat(),
- folderIndent = min(indentLevel, MAX_SUB_FOLDERS_INDENT),
- )
- }
- }
-
- private fun SelectableItemView.setFolderUi(
- folder: Folder,
- @DrawableRes iconId: Int,
- unread: UnreadDisplay?,
- trackerName: String,
- trackerValue: Float? = null,
- folderIndent: Int = 0,
- ) {
- val folderName = folder.getLocalizedName(context)
-
- text = folderName
- icon = AppCompatResources.getDrawable(context, iconId)
- setSelectedState(currentFolderId == folder.id)
-
- when (this) {
- is SelectableFolderItemView -> setIndent(folderIndent)
- is UnreadFolderItemView -> {
- initOnCollapsableClickListener { onCollapseClicked?.invoke(folder.id, isCollapsed) }
- isPastilleDisplayed = unread?.shouldDisplayPastille ?: false
- unreadCount = unread?.count ?: 0
- isHidden = folder.isHidden
- isCollapsed = folder.isCollapsed
- canBeCollapsed = folder.canBeCollapsed
- setIndent(folderIndent, hasCollapsableFolder ?: false, canBeCollapsed)
- setCollapsingButtonContentDescription(folderName)
- onCollapseTransition?.invoke(false)
- }
- is SelectableMailboxItemView, is UnreadItemView -> {
- error("`${this::class.simpleName}` cannot exists here. Only Folder classes are allowed")
- }
- }
-
- setOnClickListener {
- if (isInMenuDrawer) context.trackMenuDrawerEvent(trackerName, value = trackerValue)
- onFolderClicked.invoke(folder.id)
- }
- }
-
- @SuppressLint("NotifyDataSetChanged")
- fun setFolders(newFolders: List, newCurrentFolderId: String? = null) = runCatchingRealm {
-
- foldersDiffer.submitList(newFolders)
-
- setFoldersJob?.cancel()
- setFoldersJob = globalCoroutineScope.launch {
- newCurrentFolderId?.let { currentFolderId = it }
- val newHasCollapsableFolder = newFolders.any { it.canBeCollapsed }
-
- val isFirstTime = hasCollapsableFolder == null
- val collapsableFolderExistenceHasChanged = newHasCollapsableFolder != hasCollapsableFolder
- hasCollapsableFolder = newHasCollapsableFolder
-
- if (!isFirstTime && collapsableFolderExistenceHasChanged) {
- Dispatchers.Main { notifyDataSetChanged() }
- }
- }
- }
-
- fun updateSelectedState(newCurrentFolderId: String) {
- val previousCurrentFolderId = currentFolderId
- currentFolderId = newCurrentFolderId
- previousCurrentFolderId?.let(::notifyCurrentItem)
- notifyCurrentItem(newCurrentFolderId)
- }
-
- private fun notifyCurrentItem(folderId: String) {
- val position = folders.indexOfFirst { it.id == folderId }
- notifyItemChanged(position)
- }
-
- private enum class DisplayType(val layout: Int) {
- SELECTABLE_FOLDER(R.layout.item_selectable_folder),
- MENU_DRAWER(R.layout.item_folder_menu_drawer),
- }
-
- companion object {
- private const val MAX_SUB_FOLDERS_INDENT = 2
- }
-
- class FolderViewHolder(val binding: ViewBinding) : ViewHolder(binding.root)
-
- private class FolderDiffCallback : DiffUtil.ItemCallback() {
-
- override fun areItemsTheSame(oldFolder: Folder, newFolder: Folder) = runCatchingRealm {
- return oldFolder.id == newFolder.id
- }.getOrDefault(false)
-
- override fun areContentsTheSame(oldFolder: Folder, newFolder: Folder) = runCatchingRealm {
- oldFolder.name == newFolder.name &&
- oldFolder.isFavorite == newFolder.isFavorite &&
- oldFolder.path == newFolder.path &&
- oldFolder.unreadCountDisplay == newFolder.unreadCountDisplay &&
- oldFolder.threads.count() == newFolder.threads.count() &&
- oldFolder.isHidden == newFolder.isHidden &&
- oldFolder.canBeCollapsed == newFolder.canBeCollapsed
- }.getOrDefault(false)
- }
-}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerFragment.kt
deleted file mode 100644
index 2c0730a63b..0000000000
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerFragment.kt
+++ /dev/null
@@ -1,355 +0,0 @@
-/*
- * Infomaniak Mail - Android
- * Copyright (C) 2022-2024 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package com.infomaniak.mail.ui.main.menu
-
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.os.Bundle
-import android.transition.ChangeBounds
-import android.transition.Fade
-import android.transition.Fade.IN
-import android.transition.TransitionManager
-import android.transition.TransitionSet
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.content.pm.ShortcutManagerCompat
-import androidx.core.content.res.ResourcesCompat
-import androidx.core.view.isGone
-import androidx.core.view.isVisible
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.RecyclerView
-import com.infomaniak.lib.bugtracker.BugTrackerActivity
-import com.infomaniak.lib.bugtracker.BugTrackerActivityArgs
-import com.infomaniak.lib.core.utils.UtilsUi.openUrl
-import com.infomaniak.lib.core.utils.context
-import com.infomaniak.lib.core.utils.safeNavigate
-import com.infomaniak.mail.BuildConfig
-import com.infomaniak.mail.MatomoMail.trackCreateFolderEvent
-import com.infomaniak.mail.MatomoMail.trackMenuDrawerEvent
-import com.infomaniak.mail.MatomoMail.trackScreen
-import com.infomaniak.mail.MatomoMail.trackSyncAutoConfigEvent
-import com.infomaniak.mail.R
-import com.infomaniak.mail.databinding.FragmentMenuDrawerBinding
-import com.infomaniak.mail.ui.MainActivity
-import com.infomaniak.mail.ui.main.MailboxListFragment
-import com.infomaniak.mail.ui.main.folder.ThreadListFragmentDirections
-import com.infomaniak.mail.utils.AccountUtils
-import com.infomaniak.mail.utils.ConfettiUtils
-import com.infomaniak.mail.utils.ConfettiUtils.ConfettiType
-import com.infomaniak.mail.utils.Utils
-import com.infomaniak.mail.utils.Utils.Shortcuts
-import com.infomaniak.mail.utils.Utils.runCatchingRealm
-import com.infomaniak.mail.utils.extensions.launchSyncAutoConfigActivityForResult
-import com.infomaniak.mail.utils.extensions.observeNotNull
-import com.infomaniak.mail.utils.extensions.toggleChevron
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.launch
-
-@AndroidEntryPoint
-class MenuDrawerFragment : MenuFoldersFragment(), MailboxListFragment {
-
- private var _binding: FragmentMenuDrawerBinding? = null
- private val binding get() = _binding!! // This property is only valid between onCreateView and onDestroyView
- private val menuDrawerViewModel: MenuDrawerViewModel by viewModels()
-
- var exitDrawer: (() -> Unit)? = null
-
- override val isInMenuDrawer = true
- override val defaultFoldersList: RecyclerView by lazy { binding.defaultFoldersList }
- override val customFoldersList: RecyclerView by lazy { binding.customFoldersList }
-
- override val hasValidMailboxes = true
- override val currentClassName: String = MenuDrawerFragment::class.java.name
- override val mailboxesAdapter get() = binding.mailboxList.adapter as MailboxesAdapter
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- return FragmentMenuDrawerBinding.inflate(inflater, container, false).also { _binding = it }.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- displayVersion()
- setupListeners()
- setupCreateFolderDialog()
-
- observeCurrentMailbox()
- observeMailboxesLive()
- observeCurrentFolder()
- observeFoldersLive()
- observeQuotasLive()
- observePermissionsLive()
- observeNewFolderCreation()
- }
-
- private fun setupListeners() = with(binding) {
-
- settingsButton.setOnClickListener {
- closeDrawer()
- safeNavigate(
- directions = ThreadListFragmentDirections.actionThreadListFragmentToSettingsFragment(),
- currentClassName = currentClassName,
- )
- }
-
- mailboxSwitcher.setOnClickListener {
- addMenuDrawerTransition()
- mailboxList.apply {
- isVisible = !isVisible
- mailboxExpandButton.toggleChevron(!isVisible)
- setMailboxSwitcherTextAppearance(isVisible)
- trackMenuDrawerEvent("mailboxes", isVisible)
- }
- }
-
- customFolders.setOnClickListener {
- addMenuDrawerTransition()
- trackMenuDrawerEvent("customFolders", !customFolders.isCollapsed)
- customFoldersLayout.isGone = customFolders.isCollapsed
- }
-
- customFolders.setOnActionClickListener {
- trackCreateFolderEvent("fromMenuDrawer")
- inputDialog.show(R.string.newFolderDialogTitle, R.string.newFolderDialogHint, R.string.buttonCreate)
- }
-
- feedback.setOnClickListener {
- closeDrawer()
- if (AccountUtils.currentUser?.isStaff == true) {
- Intent(context, BugTrackerActivity::class.java).apply {
- putExtras(
- BugTrackerActivityArgs(
- user = AccountUtils.currentUser!!,
- appBuildNumber = BuildConfig.VERSION_NAME,
- bucketIdentifier = BuildConfig.BUGTRACKER_MAIL_BUCKET_ID,
- projectName = BuildConfig.BUGTRACKER_MAIL_PROJECT_NAME,
- repoGitHub = BuildConfig.GITHUB_REPO,
- ).toBundle(),
- )
- }.also(::startActivity)
- } else {
- trackMenuDrawerEvent("feedback")
- context.openUrl(getString(R.string.urlUserReportAndroid))
- }
- }
-
- help.setOnClickListener {
- ShortcutManagerCompat.reportShortcutUsed(requireContext(), Shortcuts.SUPPORT.id)
- trackMenuDrawerEvent("help")
- context.openUrl(BuildConfig.CHATBOT_URL)
- closeDrawer()
- }
-
- advancedActions.setOnClickListener {
- addMenuDrawerTransition()
- trackMenuDrawerEvent("advancedActions", !advancedActions.isCollapsed)
- advancedActionsLayout.isGone = advancedActions.isCollapsed
- }
-
- syncAutoConfig.setOnClickListener {
- trackSyncAutoConfigEvent("openFromMenuDrawer")
- launchSyncAutoConfigActivityForResult()
- closeDrawer()
- }
-
- importMails.setOnClickListener {
- trackMenuDrawerEvent("importEmails")
- requireContext().openUrl(BuildConfig.IMPORT_EMAILS_URL)
- closeDrawer()
- }
-
- restoreMails.setOnClickListener {
- trackMenuDrawerEvent("restoreEmails")
- safeNavigate(R.id.restoreEmailsBottomSheetDialog, currentClassName = currentClassName)
- }
-
- appVersionName.setOnClickListener {
- ConfettiUtils.onEasterEggConfettiClicked(
- container = (activity as? MainActivity)?.getConfettiContainer(),
- type = ConfettiType.INFOMANIAK,
- matomoValue = "MenuDrawer",
- )
- }
- }
-
- override fun onDestroyView() {
- TransitionManager.endTransitions(binding.drawerContentScrollView)
- super.onDestroyView()
- _binding = null
- }
-
- private fun setupCreateFolderDialog() {
- inputDialog.setCallbacks(
- onPositiveButtonClicked = { folderName ->
- trackCreateFolderEvent("confirm")
- mainViewModel.createNewFolder(folderName)
- },
- onErrorCheck = ::checkForFolderCreationErrors,
- )
- }
-
- override fun setupAdapters() {
- super.setupAdapters()
- binding.mailboxList.adapter = MailboxesAdapter(
- isInMenuDrawer = isInMenuDrawer,
- hasValidMailboxes = hasValidMailboxes,
- onValidMailboxClicked = { mailboxId -> onValidMailboxClicked(mailboxId) },
- onLockedMailboxClicked = { mailboxEmail -> onLockedMailboxClicked(mailboxEmail) },
- onInvalidPasswordMailboxClicked = { mailbox -> onInvalidPasswordMailboxClicked(mailbox) },
- )
- }
-
- override fun onFolderSelected(folderId: String) {
- mainViewModel.openFolder(folderId)
- closeDrawer()
- }
-
- override fun onFolderCollapse(folderId: String, shouldCollapse: Boolean) {
- menuDrawerViewModel.toggleFolderCollapsingState(folderId, shouldCollapse)
- }
-
- private fun addMenuDrawerTransition(shouldExclude: Boolean = false, shouldFade: Boolean = true) {
- val transition = TransitionSet()
- .addTransition(ChangeBounds())
- .also { if (shouldFade) it.addTransition(Fade(IN)) }
- .excludeTarget(RecyclerView::class.java, shouldExclude)
- .setDuration(MENU_DRAWER_TRANSITION_DURATION)
-
- TransitionManager.beginDelayedTransition(binding.drawerContentScrollView, transition)
- }
-
- @SuppressLint("SetTextI18n")
- private fun displayVersion() {
- binding.appVersionName.text = "App version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
- }
-
- private fun observeCurrentMailbox() {
- mainViewModel.currentMailbox.observeNotNull(viewLifecycleOwner) { mailbox ->
- binding.mailboxSwitcherText.text = mailbox.email
-
- // Make sure you always cancel all mailbox current notifications, whenever it is visible by the user.
- // Also cancel notifications from the current mailbox if it no longer exists.
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- mainViewModel.dismissCurrentMailboxNotifications()
- }
- }
- }
- }
-
- private fun observeMailboxesLive() = with(binding) {
- menuDrawerViewModel.otherMailboxesLive.observe(viewLifecycleOwner) { mailboxes ->
-
- mailboxesAdapter.setMailboxes(mailboxes)
-
- val hasMoreThanOneMailbox = mailboxes.isNotEmpty()
-
- mailboxSwitcher.apply {
- isClickable = hasMoreThanOneMailbox
- isFocusable = hasMoreThanOneMailbox
- }
-
- mailboxExpandButton.isVisible = hasMoreThanOneMailbox
- }
- }
-
- private fun observeCurrentFolder() {
- mainViewModel.currentFolder.observeNotNull(viewLifecycleOwner) { folder ->
- defaultFoldersAdapter.updateSelectedState(folder.id)
- customFoldersAdapter.updateSelectedState(folder.id)
- }
- }
-
- private fun observeFoldersLive() = with(mainViewModel) {
- Utils.waitInitMediator(
- currentFolder,
- currentDefaultFoldersLive,
- ).observe(viewLifecycleOwner) { (currentFolder, defaultFolders) ->
- runCatchingRealm {
- val newCurrentFolderId = currentFolder?.id ?: return@observe
- binding.defaultFoldersList.post {
- defaultFoldersAdapter.setFolders(defaultFolders, newCurrentFolderId)
- }
- }
- }
-
- Utils.waitInitMediator(
- currentFolder,
- currentCustomFoldersLive,
- ).observe(viewLifecycleOwner) { (currentFolder, customFolders) ->
- runCatchingRealm {
- binding.noFolderText.isVisible = customFolders.isEmpty()
- val newCurrentFolderId = currentFolder?.id ?: return@observe
- binding.customFoldersList.post {
- customFoldersAdapter.setFolders(customFolders, newCurrentFolderId)
- }
- }
- }
- }
-
- private fun observeQuotasLive() = with(binding) {
- mainViewModel.currentQuotasLive.observe(viewLifecycleOwner) { quotas ->
- val isLimited = quotas != null
-
- storageLayout.isVisible = isLimited
- storageDivider.isVisible = isLimited
-
- if (isLimited) {
- storageText.text = quotas!!.getText(context)
- storageIndicator.progress = quotas.getProgress()
- }
- }
- }
-
- private fun observePermissionsLive() {
- mainViewModel.currentPermissionsLive.observe(viewLifecycleOwner) { permissions ->
- binding.restoreMails.isVisible = permissions?.canRestoreEmails == true
- }
- }
-
- private fun observeNewFolderCreation() {
- mainViewModel.newFolderResultTrigger.observe(viewLifecycleOwner) { inputDialog.resetLoadingAndDismiss() }
- }
-
- fun onDrawerOpened() {
- trackScreen()
- }
-
- fun closeDrawer() {
- exitDrawer?.invoke()
- closeDropdowns()
- }
-
- fun closeDropdowns() = with(binding) {
- mailboxList.isGone = true
- mailboxExpandButton.rotation = ResourcesCompat.getFloat(resources, R.dimen.angleViewNotRotated)
- setMailboxSwitcherTextAppearance(isOpen = false)
- }
-
- private fun setMailboxSwitcherTextAppearance(isOpen: Boolean) {
- binding.mailboxSwitcherText.setTextAppearance(if (isOpen) R.style.BodyMedium_Accent else R.style.BodyMedium)
- }
-
- companion object {
- private const val MENU_DRAWER_TRANSITION_DURATION = 250L
- }
-}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuFoldersFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuFoldersFragment.kt
deleted file mode 100644
index 04121926ec..0000000000
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuFoldersFragment.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Infomaniak Mail - Android
- * Copyright (C) 2023-2024 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package com.infomaniak.mail.ui.main.menu
-
-import android.os.Bundle
-import android.view.View
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.recyclerview.widget.RecyclerView
-import com.infomaniak.mail.R
-import com.infomaniak.mail.data.cache.mailboxContent.FolderController
-import com.infomaniak.mail.ui.MainViewModel
-import com.infomaniak.mail.ui.alertDialogs.InputAlertDialog
-import com.infomaniak.mail.utils.extensions.bindAlertToViewLifecycle
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-
-@AndroidEntryPoint
-abstract class MenuFoldersFragment : Fragment() {
-
- @Inject
- lateinit var defaultFolderAdapter: FolderAdapter
-
- @Inject
- lateinit var customFolderAdapter: FolderAdapter
-
- @Inject
- lateinit var folderController: FolderController
-
- @Inject
- lateinit var inputDialog: InputAlertDialog
-
- protected val mainViewModel: MainViewModel by activityViewModels()
-
- protected abstract val defaultFoldersList: RecyclerView
- protected abstract val customFoldersList: RecyclerView
-
- protected abstract val isInMenuDrawer: Boolean
-
- protected val defaultFoldersAdapter: FolderAdapter by lazy {
- defaultFolderAdapter(
- isInMenuDrawer,
- onFolderClicked = ::onFolderSelected,
- onCollapseClicked = ::onFolderCollapse,
- )
- }
-
- protected val customFoldersAdapter: FolderAdapter by lazy {
- customFolderAdapter(
- isInMenuDrawer,
- onFolderClicked = ::onFolderSelected,
- onCollapseClicked = ::onFolderCollapse,
- )
- }
-
- protected abstract fun onFolderSelected(folderId: String)
-
- protected abstract fun onFolderCollapse(folderId: String, shouldCollapse: Boolean)
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- setupAdapters()
- bindAlertToViewLifecycle(inputDialog)
- }
-
- open fun setupAdapters() {
- defaultFoldersList.adapter = defaultFoldersAdapter
- customFoldersList.adapter = customFoldersAdapter
- }
-
- /**
- * Asynchronously validate folder name locally
- * @return error string, otherwise null
- */
- protected fun checkForFolderCreationErrors(folderName: CharSequence): String? = when {
- folderName.length > 255 -> getString(R.string.errorNewFolderNameTooLong)
- folderName.contains(Regex(INVALID_CHARACTERS_PATTERN)) -> getString(R.string.errorNewFolderInvalidCharacter)
- folderController.getRootFolder(folderName.toString()) != null -> context?.getString(R.string.errorNewFolderAlreadyExists)
- else -> null
- }
-
- companion object {
- private const val INVALID_CHARACTERS_PATTERN = "[/'\"]"
- }
-}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MailboxesAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MailboxesAdapter.kt
similarity index 92%
rename from app/src/main/java/com/infomaniak/mail/ui/main/menu/MailboxesAdapter.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MailboxesAdapter.kt
index 91beb933fd..43d0fc30b2 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MailboxesAdapter.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MailboxesAdapter.kt
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.infomaniak.mail.ui.main.menu
+package com.infomaniak.mail.ui.main.menuDrawer
import android.view.LayoutInflater
import android.view.View.OnClickListener
@@ -29,9 +29,9 @@ import com.infomaniak.mail.MatomoMail.trackMenuDrawerEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.databinding.ItemInvalidMailboxBinding
-import com.infomaniak.mail.databinding.ItemMailboxMenuDrawerBinding
+import com.infomaniak.mail.databinding.ItemMenuDrawerMailboxBinding
import com.infomaniak.mail.databinding.ItemSelectableMailboxBinding
-import com.infomaniak.mail.ui.main.menu.MailboxesAdapter.MailboxesViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.MailboxesAdapter.MailboxesViewHolder
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.Utils.runCatchingRealm
import com.infomaniak.mail.views.itemViews.DecoratedItemView.SelectionStyle
@@ -50,7 +50,7 @@ class MailboxesAdapter(
val layoutInflater = LayoutInflater.from(parent.context)
val binding = when (viewType) {
DisplayType.SIMPLE_MAILBOX.layout -> ItemSelectableMailboxBinding.inflate(layoutInflater, parent, false)
- DisplayType.MENU_DRAWER_MAILBOX.layout -> ItemMailboxMenuDrawerBinding.inflate(layoutInflater, parent, false)
+ DisplayType.MENU_DRAWER_MAILBOX.layout -> ItemMenuDrawerMailboxBinding.inflate(layoutInflater, parent, false)
else -> ItemInvalidMailboxBinding.inflate(layoutInflater, parent, false)
}
@@ -67,7 +67,7 @@ class MailboxesAdapter(
(this as ItemSelectableMailboxBinding).displaySimpleMailbox(mailbox, isCurrentMailbox)
}
DisplayType.MENU_DRAWER_MAILBOX.layout -> {
- (this as ItemMailboxMenuDrawerBinding).displayMenuDrawerMailbox(mailbox, isCurrentMailbox)
+ (this as ItemMenuDrawerMailboxBinding).displayMenuDrawerMailbox(mailbox, isCurrentMailbox)
}
DisplayType.INVALID_MAILBOX.layout -> (this as ItemInvalidMailboxBinding).displayInvalidMailbox(mailbox)
}
@@ -94,7 +94,7 @@ class MailboxesAdapter(
setSelectedState(isCurrentMailbox)
}
- private fun ItemMailboxMenuDrawerBinding.displayMenuDrawerMailbox(mailbox: Mailbox, isCurrentMailbox: Boolean) = with(root) {
+ private fun ItemMenuDrawerMailboxBinding.displayMenuDrawerMailbox(mailbox: Mailbox, isCurrentMailbox: Boolean) = with(root) {
displayValidMailbox(mailbox, isCurrentMailbox) { context.trackMenuDrawerEvent(SWITCH_MAILBOX_NAME) }
unreadCount = mailbox.unreadCountDisplay.count
@@ -141,7 +141,7 @@ class MailboxesAdapter(
private enum class DisplayType(val layout: Int) {
INVALID_MAILBOX(R.layout.item_invalid_mailbox),
- MENU_DRAWER_MAILBOX(R.layout.item_mailbox_menu_drawer),
+ MENU_DRAWER_MAILBOX(R.layout.item_menu_drawer_mailbox),
SIMPLE_MAILBOX(R.layout.item_selectable_mailbox),
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerAdapter.kt
new file mode 100644
index 0000000000..75d0f31375
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerAdapter.kt
@@ -0,0 +1,345 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.viewbinding.ViewBinding
+import com.infomaniak.mail.R
+import com.infomaniak.mail.data.models.Folder
+import com.infomaniak.mail.data.models.mailbox.Mailbox
+import com.infomaniak.mail.data.models.mailbox.MailboxPermissions
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerFragment.MediatorContainer
+import com.infomaniak.mail.ui.main.menuDrawer.items.ActionViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.ActionViewHolder.MenuDrawerAction
+import com.infomaniak.mail.ui.main.menuDrawer.items.ActionViewHolder.MenuDrawerAction.ActionType
+import com.infomaniak.mail.ui.main.menuDrawer.items.ActionsHeaderViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.DividerItemViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.EmptyFoldersViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.FolderViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.FoldersHeaderViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.FooterViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.FooterViewHolder.MenuDrawerFooter
+import com.infomaniak.mail.ui.main.menuDrawer.items.InvalidMailboxViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.MailboxViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.MailboxesHeaderViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.MailboxesHeaderViewHolder.MailboxesHeader
+import com.infomaniak.mail.utils.AccountUtils
+import com.infomaniak.mail.utils.Utils.runCatchingRealm
+import javax.inject.Inject
+
+class MenuDrawerAdapter @Inject constructor() : ListAdapter(FolderDiffCallback()) {
+
+ private var currentFolderId: String? = null
+ private var hasCollapsableDefaultFolder = false
+ private var hasCollapsableCustomFolder = false
+
+ private lateinit var callbacks: MenuDrawerAdapterCallbacks
+
+ operator fun invoke(callbacks: MenuDrawerAdapterCallbacks): MenuDrawerAdapter {
+ this.callbacks = callbacks
+ return this
+ }
+
+ fun formatList(mediatorContainer: MediatorContainer) = buildList {
+ runCatchingRealm {
+
+ val (
+ mailboxes,
+ areMailboxesExpanded,
+ defaultFolders,
+ customFolders,
+ areCustomFoldersExpanded,
+ areActionsExpanded,
+ permissions,
+ quotas,
+ ) = mediatorContainer
+
+ addMailboxes(mailboxes, areMailboxesExpanded)
+
+ add(ItemType.DIVIDER)
+ hasCollapsableDefaultFolder = addDefaultFolders(defaultFolders)
+
+ add(ItemType.DIVIDER)
+ hasCollapsableCustomFolder = addCustomFolders(customFolders, areCustomFoldersExpanded)
+
+ add(ItemType.DIVIDER)
+ addAdvancedActions(areActionsExpanded, permissions)
+
+ add(MenuDrawerFooter(quotas))
+ }
+ }
+
+ private fun MutableList.addMailboxes(mailboxes: List, areMailboxesExpanded: Boolean) {
+ val currentMailboxIndex = mailboxes.indexOfFirst { it.mailboxId == AccountUtils.currentMailboxId }
+ val otherMailboxes = mailboxes.toMutableList()
+ val currentMailbox = otherMailboxes.removeAt(currentMailboxIndex)
+
+ add(MailboxesHeader(currentMailbox, otherMailboxes.isNotEmpty(), areMailboxesExpanded))
+ if (areMailboxesExpanded) addAll(otherMailboxes)
+ }
+
+ private fun MutableList.addDefaultFolders(defaultFolders: List): Boolean {
+ var atLeastOneFolderIsIndented = false
+
+ defaultFolders.forEach { defaultFolder ->
+ if (defaultFolder.canBeCollapsed) atLeastOneFolderIsIndented = true
+ add(defaultFolder)
+ }
+
+ return atLeastOneFolderIsIndented
+ }
+
+ private fun MutableList.addCustomFolders(customFolders: List, areCustomFoldersExpanded: Boolean): Boolean {
+ var atLeastOneFolderIsIndented = false
+
+ add(ItemType.FOLDERS_HEADER)
+ if (!areCustomFoldersExpanded) return false
+
+ if (customFolders.isEmpty()) {
+ add(ItemType.EMPTY_FOLDERS)
+ } else {
+ customFolders.forEach { customFolder ->
+ if (customFolder.canBeCollapsed) atLeastOneFolderIsIndented = true
+ add(customFolder)
+ }
+ }
+
+ return atLeastOneFolderIsIndented
+ }
+
+ private fun MutableList.addAdvancedActions(areActionsExpanded: Boolean, permissions: MailboxPermissions?) {
+
+ add(ItemType.ACTIONS_HEADER)
+
+ if (areActionsExpanded) {
+ add(SYNC_AUTO_CONFIG_ACTION)
+ add(IMPORT_MAILS_ACTION)
+ if (permissions?.canRestoreEmails == true) add(RESTORE_MAILS_ACTION)
+ }
+ }
+
+ fun notifySelectedFolder(currentFolder: Folder) {
+
+ val oldId = currentFolderId
+ val newId = currentFolder.id
+
+ if (newId != oldId) {
+ currentFolderId = newId
+
+ var oldIsFound = false
+ var newIsFound = false
+ for (index in currentList.indices) {
+
+ if (getItemViewType(index) != ItemType.FOLDER.ordinal) continue
+
+ val itemId = (currentList[index] as Folder).id
+ if (itemId == oldId) {
+ oldIsFound = true
+ notifyItemChanged(index)
+ } else if (itemId == newId) {
+ newIsFound = true
+ notifyItemChanged(index)
+ }
+
+ if (oldIsFound && newIsFound) break
+ }
+ }
+ }
+
+ override fun getItemCount(): Int = currentList.size
+
+ override fun getItemViewType(position: Int): Int = runCatchingRealm {
+ return@runCatchingRealm when (val item = currentList[position]) {
+ ItemType.DIVIDER -> ItemType.DIVIDER.ordinal
+ is MailboxesHeader -> ItemType.MAILBOXES_HEADER.ordinal
+ is Mailbox -> if (item.isValid) ItemType.MAILBOX.ordinal else ItemType.INVALID_MAILBOX.ordinal
+ ItemType.FOLDERS_HEADER -> ItemType.FOLDERS_HEADER.ordinal
+ is Folder -> ItemType.FOLDER.ordinal
+ ItemType.EMPTY_FOLDERS -> ItemType.EMPTY_FOLDERS.ordinal
+ ItemType.ACTIONS_HEADER -> ItemType.ACTIONS_HEADER.ordinal
+ is MenuDrawerAction -> ItemType.ACTION.ordinal
+ is MenuDrawerFooter -> ItemType.FOOTER.ordinal
+ else -> error("Failed to find a viewType for MenuDrawer item")
+ }
+ }.getOrDefault(super.getItemViewType(position))
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MenuDrawerViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+
+ return when (viewType) {
+ ItemType.DIVIDER.ordinal -> DividerItemViewHolder(inflater, parent)
+ ItemType.MAILBOXES_HEADER.ordinal -> MailboxesHeaderViewHolder(inflater, parent)
+ ItemType.MAILBOX.ordinal -> MailboxViewHolder(inflater, parent)
+ ItemType.INVALID_MAILBOX.ordinal -> InvalidMailboxViewHolder(inflater, parent)
+ ItemType.FOLDERS_HEADER.ordinal -> FoldersHeaderViewHolder(inflater, parent)
+ ItemType.FOLDER.ordinal -> FolderViewHolder(inflater, parent)
+ ItemType.EMPTY_FOLDERS.ordinal -> EmptyFoldersViewHolder(inflater, parent)
+ ItemType.ACTIONS_HEADER.ordinal -> ActionsHeaderViewHolder(inflater, parent)
+ ItemType.ACTION.ordinal -> ActionViewHolder(inflater, parent)
+ ItemType.FOOTER.ordinal -> FooterViewHolder(inflater, parent)
+ else -> error("Failed to find a binding for MenuDrawer viewType")
+ }
+ }
+
+ override fun onBindViewHolder(holder: MenuDrawerViewHolder, position: Int, payloads: MutableList) {
+ if (payloads.firstOrNull() == NotifyType.MAILBOXES_HEADER_CLICKED) {
+ (holder as MailboxesHeaderViewHolder).updateCollapseState(header = currentList[position] as MailboxesHeader)
+ } else {
+ super.onBindViewHolder(holder, position, payloads)
+ }
+ }
+
+ override fun onBindViewHolder(holder: MenuDrawerViewHolder, position: Int) {
+ val item = currentList[position]
+
+ when (holder) {
+ is MailboxesHeaderViewHolder -> holder.displayMailboxesHeader(
+ header = item as MailboxesHeader,
+ onMailboxesHeaderClicked = callbacks.onMailboxesHeaderClicked,
+ )
+ is MailboxViewHolder -> holder.displayMailbox(
+ mailbox = item as Mailbox,
+ onValidMailboxClicked = callbacks.onValidMailboxClicked,
+ )
+ is InvalidMailboxViewHolder -> holder.displayInvalidMailbox(
+ mailbox = item as Mailbox,
+ onLockedMailboxClicked = callbacks.onLockedMailboxClicked,
+ onInvalidPasswordMailboxClicked = callbacks.onInvalidPasswordMailboxClicked,
+ )
+ is FoldersHeaderViewHolder -> holder.displayFoldersHeader(
+ onFoldersHeaderClicked = callbacks.onFoldersHeaderClicked,
+ onCreateFolderClicked = callbacks.onCreateFolderClicked,
+ )
+ is FolderViewHolder -> holder.displayFolder(
+ folder = item as Folder,
+ currentFolderId = currentFolderId,
+ hasCollapsableFolder = if (item.role == null) hasCollapsableCustomFolder else hasCollapsableDefaultFolder,
+ onFolderClicked = callbacks.onFolderClicked,
+ onCollapseChildrenClicked = callbacks.onCollapseChildrenClicked,
+ )
+ is ActionsHeaderViewHolder -> holder.displayActionsHeader(
+ onActionsHeaderClicked = callbacks.onActionsHeaderClicked,
+ )
+ is ActionViewHolder -> holder.displayAction(
+ action = item as MenuDrawerAction,
+ onActionClicked = callbacks.onActionClicked,
+ )
+ is FooterViewHolder -> holder.displayFooter(
+ footer = item as MenuDrawerFooter,
+ onFeedbackClicked = callbacks.onFeedbackClicked,
+ onHelpClicked = callbacks.onHelpClicked,
+ onAppVersionClicked = callbacks.onAppVersionClicked,
+ )
+ }
+ }
+
+ abstract class MenuDrawerViewHolder(open val binding: ViewBinding) : ViewHolder(binding.root)
+
+ enum class ItemType {
+ DIVIDER,
+ MAILBOXES_HEADER,
+ MAILBOX,
+ INVALID_MAILBOX,
+ FOLDERS_HEADER,
+ FOLDER,
+ EMPTY_FOLDERS,
+ ACTIONS_HEADER,
+ ACTION,
+ FOOTER,
+ }
+
+ private enum class NotifyType {
+ MAILBOXES_HEADER_CLICKED,
+ }
+
+ private class FolderDiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: Any, newItem: Any) = runCatchingRealm {
+ when (oldItem) {
+ ItemType.DIVIDER -> newItem == ItemType.DIVIDER
+ is MailboxesHeader -> newItem is MailboxesHeader && newItem.mailbox?.objectId == oldItem.mailbox?.objectId
+ is Mailbox -> newItem is Mailbox && newItem.objectId == oldItem.objectId
+ ItemType.FOLDERS_HEADER -> newItem == ItemType.FOLDERS_HEADER
+ is Folder -> newItem is Folder && newItem.id == oldItem.id
+ ItemType.EMPTY_FOLDERS -> newItem == ItemType.EMPTY_FOLDERS
+ ItemType.ACTIONS_HEADER -> newItem == ItemType.ACTIONS_HEADER
+ is MenuDrawerAction -> newItem is MenuDrawerAction && newItem.type == oldItem.type
+ is MenuDrawerFooter -> newItem is MenuDrawerFooter
+ else -> error("oldItem wasn't any known item type (in MenuDrawer `areItemsTheSame`)")
+ }
+ }.getOrDefault(false)
+
+ override fun areContentsTheSame(oldItem: Any, newItem: Any) = runCatchingRealm {
+ when (oldItem) {
+ is MailboxesHeader -> newItem is MailboxesHeader
+ && newItem.hasMoreThanOneMailbox == oldItem.hasMoreThanOneMailbox
+ && newItem.isExpanded == oldItem.isExpanded
+ && newItem.mailbox?.unreadCountDisplay?.count == oldItem.mailbox?.unreadCountDisplay?.count
+ is Mailbox -> newItem is Mailbox && newItem.unreadCountDisplay.count == oldItem.unreadCountDisplay.count
+ is Folder -> newItem is Folder &&
+ newItem.name == oldItem.name &&
+ newItem.isFavorite == oldItem.isFavorite &&
+ newItem.path == oldItem.path &&
+ newItem.unreadCountDisplay == oldItem.unreadCountDisplay &&
+ newItem.threads.count() == oldItem.threads.count() &&
+ newItem.canBeCollapsed == oldItem.canBeCollapsed
+ is MenuDrawerFooter -> newItem is MenuDrawerFooter && newItem.quotas?.size == oldItem.quotas?.size
+ ItemType.DIVIDER,
+ ItemType.FOLDERS_HEADER,
+ ItemType.EMPTY_FOLDERS,
+ ItemType.ACTIONS_HEADER,
+ is MenuDrawerAction -> true
+ else -> error("oldItem wasn't any known item type (in MenuDrawer `areContentsTheSame`)")
+ }
+ }.getOrDefault(false)
+
+ override fun getChangePayload(oldItem: Any, newItem: Any): Any? {
+ return if (newItem is MailboxesHeader && oldItem is MailboxesHeader && newItem.isExpanded != oldItem.isExpanded) {
+ NotifyType.MAILBOXES_HEADER_CLICKED
+ } else {
+ null
+ }
+ }
+ }
+
+ companion object {
+ private val SYNC_AUTO_CONFIG_ACTION = MenuDrawerAction(
+ type = ActionType.SYNC_AUTO_CONFIG,
+ icon = R.drawable.ic_synchronize,
+ text = R.string.syncCalendarsAndContactsTitle,
+ maxLines = 2,
+ )
+ private val IMPORT_MAILS_ACTION = MenuDrawerAction(
+ type = ActionType.IMPORT_MAILS,
+ icon = R.drawable.ic_drawer_download,
+ text = R.string.buttonImportEmails,
+ maxLines = 1,
+ )
+ private val RESTORE_MAILS_ACTION = MenuDrawerAction(
+ type = ActionType.RESTORE_MAILS,
+ icon = R.drawable.ic_restore_arrow,
+ text = R.string.buttonRestoreEmails,
+ maxLines = 1,
+ )
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerAdapterCallbacks.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerAdapterCallbacks.kt
new file mode 100644
index 0000000000..f470fe2d99
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerAdapterCallbacks.kt
@@ -0,0 +1,43 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer
+
+import com.infomaniak.mail.data.models.mailbox.Mailbox
+import com.infomaniak.mail.ui.main.menuDrawer.items.ActionViewHolder.MenuDrawerAction.ActionType
+
+interface MenuDrawerAdapterCallbacks {
+
+ var onMailboxesHeaderClicked: () -> Unit
+
+ var onValidMailboxClicked: (Int) -> Unit
+ var onLockedMailboxClicked: (String) -> Unit
+ var onInvalidPasswordMailboxClicked: (Mailbox) -> Unit
+
+ var onFoldersHeaderClicked: (Boolean) -> Unit
+ var onCreateFolderClicked: () -> Unit
+
+ var onFolderClicked: (folderId: String) -> Unit
+ var onCollapseChildrenClicked: (folderId: String, shouldCollapse: Boolean) -> Unit
+
+ var onActionsHeaderClicked: () -> Unit
+ var onActionClicked: (ActionType) -> Unit
+
+ var onFeedbackClicked: () -> Unit
+ var onHelpClicked: () -> Unit
+ var onAppVersionClicked: () -> Unit
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerDropdownView.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerDropdownView.kt
similarity index 98%
rename from app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerDropdownView.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerDropdownView.kt
index 90dac98361..37ceb92b07 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerDropdownView.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerDropdownView.kt
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.infomaniak.mail.ui.main.menu
+package com.infomaniak.mail.ui.main.menuDrawer
import android.content.Context
import android.util.AttributeSet
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerFragment.kt
new file mode 100644
index 0000000000..5dcfc1c058
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerFragment.kt
@@ -0,0 +1,335 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2022-2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle.State
+import androidx.lifecycle.asFlow
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.infomaniak.lib.bugtracker.BugTrackerActivity
+import com.infomaniak.lib.bugtracker.BugTrackerActivityArgs
+import com.infomaniak.lib.core.utils.UtilsUi.openUrl
+import com.infomaniak.lib.core.utils.safeNavigate
+import com.infomaniak.mail.BuildConfig
+import com.infomaniak.mail.MatomoMail.toFloat
+import com.infomaniak.mail.MatomoMail.trackCreateFolderEvent
+import com.infomaniak.mail.MatomoMail.trackMenuDrawerEvent
+import com.infomaniak.mail.MatomoMail.trackScreen
+import com.infomaniak.mail.MatomoMail.trackSyncAutoConfigEvent
+import com.infomaniak.mail.R
+import com.infomaniak.mail.data.models.Folder
+import com.infomaniak.mail.data.models.Quotas
+import com.infomaniak.mail.data.models.mailbox.Mailbox
+import com.infomaniak.mail.data.models.mailbox.MailboxPermissions
+import com.infomaniak.mail.databinding.FragmentMenuDrawerBinding
+import com.infomaniak.mail.ui.MainActivity
+import com.infomaniak.mail.ui.MainViewModel
+import com.infomaniak.mail.ui.alertDialogs.CreateFolderDialog
+import com.infomaniak.mail.ui.bottomSheetDialogs.LockedMailboxBottomSheetDialogArgs
+import com.infomaniak.mail.ui.main.InvalidPasswordFragmentArgs
+import com.infomaniak.mail.ui.main.folder.ThreadListFragmentDirections
+import com.infomaniak.mail.ui.main.menuDrawer.items.ActionViewHolder.MenuDrawerAction.ActionType
+import com.infomaniak.mail.utils.AccountUtils
+import com.infomaniak.mail.utils.ConfettiUtils
+import com.infomaniak.mail.utils.ConfettiUtils.ConfettiType.INFOMANIAK
+import com.infomaniak.mail.utils.Utils
+import com.infomaniak.mail.utils.Utils.Shortcuts
+import com.infomaniak.mail.utils.extensions.bindAlertToViewLifecycle
+import com.infomaniak.mail.utils.extensions.launchSyncAutoConfigActivityForResult
+import com.infomaniak.mail.utils.extensions.observeNotNull
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class MenuDrawerFragment : Fragment() {
+
+ private var _binding: FragmentMenuDrawerBinding? = null
+ private val binding get() = _binding!! // This property is only valid between onCreateView and onDestroyView
+ private val menuDrawerViewModel: MenuDrawerViewModel by viewModels()
+ private val mainViewModel: MainViewModel by activityViewModels()
+
+ @Inject
+ lateinit var createFolderDialog: CreateFolderDialog
+
+ @Inject
+ lateinit var menuDrawerAdapter: MenuDrawerAdapter
+
+ private val currentClassName: String = MenuDrawerFragment::class.java.name
+
+ var exitDrawer: (() -> Unit)? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ return FragmentMenuDrawerBinding.inflate(inflater, container, false).also { _binding = it }.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ bindAlertToViewLifecycle(createFolderDialog)
+
+ setupListeners()
+ setupCreateFolderDialog()
+ setupRecyclerView()
+
+ observeMenuDrawerData()
+ observeCurrentFolder()
+ observeCurrentMailbox()
+ observeNewFolderCreation()
+ }
+
+ private fun setupListeners() {
+ binding.settingsButton.setOnClickListener {
+ closeDrawer()
+ safeNavigate(
+ directions = ThreadListFragmentDirections.actionThreadListFragmentToSettingsFragment(),
+ currentClassName = currentClassName,
+ )
+ }
+ }
+
+ private fun setupCreateFolderDialog() {
+ createFolderDialog.setCallbacks(onPositiveButtonClicked = mainViewModel::createNewFolder)
+ }
+
+ private fun setupRecyclerView() {
+ binding.menuDrawerRecyclerView.adapter = menuDrawerAdapter(
+ callbacks = object : MenuDrawerAdapterCallbacks {
+ override var onMailboxesHeaderClicked: () -> Unit = ::onMailboxesHeaderClicked
+ override var onValidMailboxClicked: (Int) -> Unit = ::onValidMailboxClicked
+ override var onLockedMailboxClicked: (String) -> Unit = ::onLockedMailboxClicked
+ override var onInvalidPasswordMailboxClicked: (Mailbox) -> Unit = ::onInvalidPasswordMailboxClicked
+ override var onFoldersHeaderClicked: (Boolean) -> Unit = ::onFoldersHeaderClicked
+ override var onCreateFolderClicked: () -> Unit = ::onCreateFolderClicked
+ override var onFolderClicked: (folderId: String) -> Unit = ::onFolderSelected
+ override var onCollapseChildrenClicked: (folderId: String, shouldCollapse: Boolean) -> Unit = ::onFolderCollapsed
+ override var onActionsHeaderClicked: () -> Unit = ::onActionsHeaderClicked
+ override var onActionClicked: (ActionType) -> Unit = ::onActionClicked
+ override var onFeedbackClicked: () -> Unit = ::onFeedbackClicked
+ override var onHelpClicked: () -> Unit = ::onHelpClicked
+ override var onAppVersionClicked: () -> Unit = ::onAppVersionClicked
+ },
+ )
+ }
+
+ fun onDrawerOpened() {
+ trackScreen()
+ }
+
+ fun closeDrawer() {
+ exitDrawer?.invoke()
+ closeDropdowns()
+ }
+
+ fun closeDropdowns() {
+ menuDrawerViewModel.areMailboxesExpanded.value = false
+ }
+
+ private fun onMailboxesHeaderClicked() = with(menuDrawerViewModel) {
+ val isExpanded = !areMailboxesExpanded.value!!
+ trackMenuDrawerEvent("mailboxes", isExpanded)
+ areMailboxesExpanded.value = isExpanded
+ }
+
+ private fun onValidMailboxClicked(mailboxId: Int) {
+ lifecycleScope.launch { AccountUtils.switchToMailbox(mailboxId) }
+ }
+
+ private fun onInvalidPasswordMailboxClicked(mailbox: Mailbox) {
+ safeNavigate(
+ resId = R.id.invalidPasswordFragment,
+ args = InvalidPasswordFragmentArgs(mailbox.mailboxId, mailbox.objectId, mailbox.email).toBundle(),
+ currentClassName = currentClassName,
+ )
+ }
+
+ private fun onLockedMailboxClicked(mailboxEmail: String) {
+ safeNavigate(
+ resId = R.id.lockedMailboxBottomSheetDialog,
+ args = LockedMailboxBottomSheetDialogArgs(mailboxEmail).toBundle(),
+ currentClassName = currentClassName,
+ )
+ }
+
+ private fun onFoldersHeaderClicked(isCollapsed: Boolean) {
+ trackMenuDrawerEvent("customFolders", !isCollapsed)
+ menuDrawerViewModel.areCustomFoldersExpanded.value = !isCollapsed
+ }
+
+ private fun onCreateFolderClicked() {
+ trackCreateFolderEvent("fromMenuDrawer")
+ createFolderDialog.show()
+ }
+
+ private fun onFolderSelected(folderId: String) {
+ mainViewModel.openFolder(folderId)
+ closeDrawer()
+ }
+
+ private fun onFolderCollapsed(folderId: String, shouldCollapse: Boolean) {
+ menuDrawerViewModel.toggleFolderCollapsingState(folderId, shouldCollapse)
+ }
+
+ private fun onActionsHeaderClicked() {
+ context?.trackMenuDrawerEvent("advancedActions", value = (!menuDrawerViewModel.areActionsExpanded.value!!).toFloat())
+ menuDrawerViewModel.toggleActionsCollapsingState()
+ }
+
+ private fun onActionClicked(type: ActionType) {
+ when (type) {
+ ActionType.SYNC_AUTO_CONFIG -> onSyncAutoConfigClicked()
+ ActionType.IMPORT_MAILS -> onImportMailsClicked()
+ ActionType.RESTORE_MAILS -> onRestoreMailsClicked()
+ }
+ }
+
+ private fun onSyncAutoConfigClicked() {
+ trackSyncAutoConfigEvent("openFromMenuDrawer")
+ launchSyncAutoConfigActivityForResult()
+ closeDrawer()
+ }
+
+ private fun onImportMailsClicked() {
+ trackMenuDrawerEvent("importEmails")
+ context?.openUrl(BuildConfig.IMPORT_EMAILS_URL)
+ closeDrawer()
+ }
+
+ private fun onRestoreMailsClicked() {
+ trackMenuDrawerEvent("restoreEmails")
+ safeNavigate(R.id.restoreEmailsBottomSheetDialog, currentClassName = currentClassName)
+ }
+
+ private fun onFeedbackClicked() {
+
+ if (AccountUtils.currentUser?.isStaff == true) {
+ Intent(requireContext(), BugTrackerActivity::class.java).apply {
+ putExtras(
+ BugTrackerActivityArgs(
+ user = AccountUtils.currentUser!!,
+ appBuildNumber = BuildConfig.VERSION_NAME,
+ bucketIdentifier = BuildConfig.BUGTRACKER_MAIL_BUCKET_ID,
+ projectName = BuildConfig.BUGTRACKER_MAIL_PROJECT_NAME,
+ repoGitHub = BuildConfig.GITHUB_REPO,
+ ).toBundle(),
+ )
+ }.also(::startActivity)
+ } else {
+ trackMenuDrawerEvent("feedback")
+ context?.openUrl(requireContext().getString(R.string.urlUserReportAndroid))
+ }
+
+ closeDrawer()
+ }
+
+ private fun onHelpClicked() {
+ ShortcutManagerCompat.reportShortcutUsed(requireContext(), Shortcuts.SUPPORT.id)
+ trackMenuDrawerEvent("help")
+ context?.openUrl(BuildConfig.CHATBOT_URL)
+ closeDrawer()
+ }
+
+ private fun onAppVersionClicked() {
+ ConfettiUtils.onEasterEggConfettiClicked(
+ container = (activity as? MainActivity)?.getConfettiContainer(),
+ type = INFOMANIAK,
+ matomoValue = "MenuDrawer",
+ )
+ }
+
+ private fun observeMenuDrawerData() = with(mainViewModel) {
+
+ Utils.waitInitMediator(
+ mailboxesLive,
+ menuDrawerViewModel.areMailboxesExpanded,
+ defaultFoldersLive,
+ customFoldersLive,
+ menuDrawerViewModel.areCustomFoldersExpanded,
+ menuDrawerViewModel.areActionsExpanded,
+ currentPermissionsLive,
+ currentQuotasLive,
+ constructor = {
+ @Suppress("UNCHECKED_CAST")
+ MediatorContainer(
+ it[0] as List,
+ it[1] as Boolean,
+ it[2] as List,
+ it[3] as List,
+ it[4] as Boolean,
+ it[5] as Boolean,
+ it[6] as MailboxPermissions?,
+ it[7] as Quotas?,
+ )
+ }
+ )
+ .asFlow()
+ .map(menuDrawerAdapter::formatList)
+ .flowOn(Dispatchers.IO)
+ .asLiveData()
+ .observe(viewLifecycleOwner, menuDrawerAdapter::submitList)
+ }
+
+ private fun observeCurrentFolder() {
+ mainViewModel.currentFolder.observeNotNull(viewLifecycleOwner, menuDrawerAdapter::notifySelectedFolder)
+ }
+
+ private fun observeCurrentMailbox() {
+ mainViewModel.currentMailbox.observeNotNull(viewLifecycleOwner) {
+ // Make sure you always cancel all Mailbox current notifications, whenever it is visible by the user.
+ // Also cancel notifications from the current mailbox if it no longer exists.
+ lifecycleScope.launch {
+ repeatOnLifecycle(State.STARTED) {
+ mainViewModel.dismissCurrentMailboxNotifications()
+ }
+ }
+ }
+ }
+
+ private fun observeNewFolderCreation() {
+ mainViewModel.newFolderResultTrigger.observe(viewLifecycleOwner) { createFolderDialog.resetLoadingAndDismiss() }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ data class MediatorContainer(
+ val mailboxes: List,
+ val areMailboxesExpanded: Boolean,
+ val defaultFolders: List,
+ val customFolders: List,
+ val areCustomFoldersExpanded: Boolean,
+ val areActionsExpanded: Boolean,
+ val permissions: MailboxPermissions?,
+ val quotas: Quotas?,
+ )
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerViewModel.kt
similarity index 79%
rename from app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerViewModel.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerViewModel.kt
index daddd8ad65..ff61829371 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MenuDrawerViewModel.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/MenuDrawerViewModel.kt
@@ -15,16 +15,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.infomaniak.mail.ui.main.menu
+package com.infomaniak.mail.ui.main.menuDrawer
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.infomaniak.mail.data.cache.RealmDatabase
import com.infomaniak.mail.data.cache.mailboxContent.FolderController
-import com.infomaniak.mail.data.cache.mailboxInfo.MailboxController
import com.infomaniak.mail.di.IoDispatcher
-import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.coroutineContext
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
@@ -34,20 +32,22 @@ import javax.inject.Inject
@HiltViewModel
class MenuDrawerViewModel @Inject constructor(
private val mailboxContentRealm: RealmDatabase.MailboxContent,
- mailboxController: MailboxController,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher)
- val otherMailboxesLive = mailboxController.getMailboxesAsync(
- userId = AccountUtils.currentUserId,
- exceptionMailboxIds = listOf(AccountUtils.currentMailboxId),
- ).asLiveData(ioCoroutineContext)
+ val areMailboxesExpanded = MutableLiveData(false)
+ val areCustomFoldersExpanded = MutableLiveData(true)
+ val areActionsExpanded = MutableLiveData(false)
fun toggleFolderCollapsingState(folderId: String, shouldCollapse: Boolean) = viewModelScope.launch(ioCoroutineContext) {
FolderController.updateFolderAndChildren(folderId, mailboxContentRealm()) {
if (it.isRoot) it.isCollapsed = shouldCollapse else it.isHidden = shouldCollapse
}
}
+
+ fun toggleActionsCollapsingState() {
+ areActionsExpanded.value = !areActionsExpanded.value!!
+ }
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/RestoreEmailsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/RestoreEmailsBottomSheetDialog.kt
similarity index 99%
rename from app/src/main/java/com/infomaniak/mail/ui/main/menu/RestoreEmailsBottomSheetDialog.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/RestoreEmailsBottomSheetDialog.kt
index 5759fb1b94..d6fda773f6 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/RestoreEmailsBottomSheetDialog.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/RestoreEmailsBottomSheetDialog.kt
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.infomaniak.mail.ui.main.menu
+package com.infomaniak.mail.ui.main.menuDrawer
import android.os.Bundle
import android.view.LayoutInflater
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/RestoreEmailsViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/RestoreEmailsViewModel.kt
similarity index 97%
rename from app/src/main/java/com/infomaniak/mail/ui/main/menu/RestoreEmailsViewModel.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/RestoreEmailsViewModel.kt
index 55c5245053..6e06fd5a04 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/RestoreEmailsViewModel.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/RestoreEmailsViewModel.kt
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.infomaniak.mail.ui.main.menu
+package com.infomaniak.mail.ui.main.menuDrawer
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/SimpleSettingView.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/SimpleSettingView.kt
similarity index 98%
rename from app/src/main/java/com/infomaniak/mail/ui/main/menu/SimpleSettingView.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/SimpleSettingView.kt
index b25695e791..62c08a4f6b 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/SimpleSettingView.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/SimpleSettingView.kt
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.infomaniak.mail.ui.main.menu
+package com.infomaniak.mail.ui.main.menuDrawer
import android.content.Context
import android.util.AttributeSet
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/ActionViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/ActionViewHolder.kt
new file mode 100644
index 0000000000..47a2834c50
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/ActionViewHolder.kt
@@ -0,0 +1,64 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.appcompat.content.res.AppCompatResources
+import com.infomaniak.lib.core.utils.SentryLog
+import com.infomaniak.mail.databinding.ItemMenuDrawerActionBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+import com.infomaniak.mail.ui.main.menuDrawer.items.ActionViewHolder.MenuDrawerAction.ActionType
+
+class ActionViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerActionBinding.inflate(inflater, parent, false)) {
+
+ override val binding = super.binding as ItemMenuDrawerActionBinding
+
+ fun displayAction(
+ action: MenuDrawerAction,
+ onActionClicked: (ActionType) -> Unit,
+ ) {
+ SentryLog.d("Bind", "Bind Action : ${action.type.name}")
+
+ binding.root.apply {
+ icon = AppCompatResources.getDrawable(context, action.icon)
+ text = context.getString(action.text)
+ maxLines = action.maxLines
+ setOnClickListener { onActionClicked(action.type) }
+ }
+ }
+
+ data class MenuDrawerAction(
+ val type: ActionType,
+ @DrawableRes val icon: Int,
+ @StringRes val text: Int,
+ val maxLines: Int,
+ ) {
+
+ enum class ActionType {
+ SYNC_AUTO_CONFIG,
+ IMPORT_MAILS,
+ RESTORE_MAILS,
+ }
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/ActionsHeaderViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/ActionsHeaderViewHolder.kt
new file mode 100644
index 0000000000..57353f8b28
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/ActionsHeaderViewHolder.kt
@@ -0,0 +1,35 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.infomaniak.lib.core.utils.SentryLog
+import com.infomaniak.mail.databinding.ItemMenuDrawerActionsHeaderBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+
+class ActionsHeaderViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerActionsHeaderBinding.inflate(inflater, parent, false)) {
+
+ fun displayActionsHeader(onActionsHeaderClicked: () -> Unit) {
+ SentryLog.d("Bind", "Bind Actions header")
+ binding.root.setOnClickListener { onActionsHeaderClicked() }
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/DividerItemViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/DividerItemViewHolder.kt
new file mode 100644
index 0000000000..ca3bf9e22c
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/DividerItemViewHolder.kt
@@ -0,0 +1,28 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.infomaniak.mail.databinding.ItemMenuDrawerDividerBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+
+class DividerItemViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerDividerBinding.inflate(inflater, parent, false))
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/EmptyFoldersViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/EmptyFoldersViewHolder.kt
new file mode 100644
index 0000000000..d10e3695be
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/EmptyFoldersViewHolder.kt
@@ -0,0 +1,28 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.infomaniak.mail.databinding.ItemMenuDrawerEmptyCustomFoldersBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+
+class EmptyFoldersViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerEmptyCustomFoldersBinding.inflate(inflater, parent, false))
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FolderViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FolderViewHolder.kt
new file mode 100644
index 0000000000..26613869b4
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FolderViewHolder.kt
@@ -0,0 +1,128 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.DrawableRes
+import com.infomaniak.lib.core.utils.SentryLog
+import com.infomaniak.mail.MatomoMail.trackMenuDrawerEvent
+import com.infomaniak.mail.R
+import com.infomaniak.mail.data.models.Folder
+import com.infomaniak.mail.data.models.Folder.FolderRole
+import com.infomaniak.mail.databinding.ItemMenuDrawerFolderBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+import com.infomaniak.mail.utils.UnreadDisplay
+import com.infomaniak.mail.views.itemViews.UnreadFolderItemView
+import com.infomaniak.mail.views.itemViews.setFolderUi
+import kotlin.math.min
+
+class FolderViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerFolderBinding.inflate(inflater, parent, false)) {
+
+ override val binding = super.binding as ItemMenuDrawerFolderBinding
+
+ fun displayFolder(
+ folder: Folder,
+ currentFolderId: String?,
+ hasCollapsableFolder: Boolean,
+ onFolderClicked: (folderId: String) -> Unit,
+ onCollapseChildrenClicked: (folderId: String, shouldCollapse: Boolean) -> Unit,
+ ) {
+ SentryLog.d("Bind", "Bind Folder : ${folder.name}")
+
+ val roleDependantParameters = folder.role?.let {
+ RoleDependantParameters(
+ iconId = it.folderIconRes,
+ trackerName = it.matomoValue,
+ trackerValue = null,
+ folderIndent = 0,
+ )
+ } ?: run {
+ val indentLevel = folder.path.split(folder.separator).size - 1
+ RoleDependantParameters(
+ iconId = if (folder.isFavorite) R.drawable.ic_folder_star else R.drawable.ic_folder,
+ trackerName = "customFolder",
+ trackerValue = indentLevel.toFloat(),
+ folderIndent = min(indentLevel, MAX_SUB_FOLDERS_INDENT),
+ )
+ }
+
+ val unread = when (folder.role) {
+ FolderRole.DRAFT -> UnreadDisplay(count = folder.threads.count())
+ FolderRole.SENT, FolderRole.TRASH -> UnreadDisplay(count = 0)
+ else -> folder.unreadCountDisplay
+ }
+
+ binding.root.setFolderUi(
+ folder,
+ roleDependantParameters,
+ unread,
+ currentFolderId,
+ hasCollapsableFolder,
+ onFolderClicked,
+ onCollapseChildrenClicked,
+ )
+ }
+
+ private fun UnreadFolderItemView.setFolderUi(
+ folder: Folder,
+ roleDependantParameters: RoleDependantParameters,
+ unread: UnreadDisplay?,
+ currentFolderId: String?,
+ hasCollapsableFolder: Boolean,
+ onFolderClicked: (folderId: String) -> Unit,
+ onCollapseChildrenClicked: (folderId: String, shouldCollapse: Boolean) -> Unit,
+ ) {
+
+ val folderName = folder.getLocalizedName(context)
+ val (iconId, trackerName, trackerValue, folderIndent) = roleDependantParameters
+
+ setFolderUi(folder, iconId, isSelected = folder.id == currentFolderId)
+
+ initOnCollapsableClickListener { onCollapseChildrenClicked(folder.id, isCollapsed) }
+ isPastilleDisplayed = unread?.shouldDisplayPastille ?: false
+ unreadCount = unread?.count ?: 0
+ isCollapsed = folder.isCollapsed
+ canBeCollapsed = folder.canBeCollapsed
+ setIndent(
+ indent = folderIndent,
+ hasCollapsableFolder = hasCollapsableFolder,
+ canBeCollapsed = canBeCollapsed,
+ )
+ setCollapsingButtonContentDescription(folderName)
+
+ setOnClickListener {
+ context.trackMenuDrawerEvent(trackerName, value = trackerValue)
+ onFolderClicked.invoke(folder.id)
+ }
+ }
+
+ private data class RoleDependantParameters(
+ @DrawableRes var iconId: Int,
+ var trackerName: String,
+ var trackerValue: Float?,
+ var folderIndent: Int,
+ )
+
+ companion object {
+ private const val MAX_SUB_FOLDERS_INDENT = 2
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FoldersHeaderViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FoldersHeaderViewHolder.kt
new file mode 100644
index 0000000000..fc9f655841
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FoldersHeaderViewHolder.kt
@@ -0,0 +1,42 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.infomaniak.lib.core.utils.SentryLog
+import com.infomaniak.mail.databinding.ItemMenuDrawerCustomFoldersHeaderBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+
+class FoldersHeaderViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerCustomFoldersHeaderBinding.inflate(inflater, parent, false)) {
+
+ override val binding = super.binding as ItemMenuDrawerCustomFoldersHeaderBinding
+
+ fun displayFoldersHeader(
+ onFoldersHeaderClicked: (Boolean) -> Unit,
+ onCreateFolderClicked: () -> Unit,
+ ) = with(binding.root) {
+ SentryLog.d("Bind", "Bind Custom Folders header")
+
+ setOnClickListener { onFoldersHeaderClicked(isCollapsed) }
+ setOnActionClickListener { onCreateFolderClicked() }
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FooterViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FooterViewHolder.kt
new file mode 100644
index 0000000000..3febef5b6e
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FooterViewHolder.kt
@@ -0,0 +1,68 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import com.infomaniak.lib.core.utils.SentryLog
+import com.infomaniak.lib.core.utils.context
+import com.infomaniak.mail.BuildConfig
+import com.infomaniak.mail.data.models.Quotas
+import com.infomaniak.mail.databinding.ItemMenuDrawerFooterBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+
+class FooterViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerFooterBinding.inflate(inflater, parent, false)) {
+
+ override val binding = super.binding as ItemMenuDrawerFooterBinding
+
+ fun displayFooter(
+ footer: MenuDrawerFooter,
+ onFeedbackClicked: () -> Unit,
+ onHelpClicked: () -> Unit,
+ onAppVersionClicked: () -> Unit,
+ ) = with(binding) {
+ SentryLog.d("Bind", "Bind Footer")
+
+ // Feedback
+ feedback.setOnClickListener { onFeedbackClicked() }
+
+ // Help
+ help.setOnClickListener { onHelpClicked() }
+
+ // Quotas
+ val isLimited = footer.quotas != null
+ storageLayout.isVisible = isLimited
+ storageDivider.isVisible = isLimited
+ if (isLimited) {
+ storageText.text = footer.quotas!!.getText(context)
+ storageIndicator.progress = footer.quotas.getProgress()
+ }
+
+ // App version
+ appVersionName.apply {
+ text = "App version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
+ setOnClickListener { onAppVersionClicked() }
+ }
+ }
+
+ data class MenuDrawerFooter(val quotas: Quotas?)
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/InvalidMailboxViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/InvalidMailboxViewHolder.kt
new file mode 100644
index 0000000000..165d9663b9
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/InvalidMailboxViewHolder.kt
@@ -0,0 +1,55 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.infomaniak.lib.core.utils.SentryLog
+import com.infomaniak.mail.data.models.mailbox.Mailbox
+import com.infomaniak.mail.databinding.ItemInvalidMailboxBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+import com.infomaniak.mail.views.itemViews.DecoratedItemView
+
+class InvalidMailboxViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemInvalidMailboxBinding.inflate(inflater, parent, false)) {
+
+ override val binding = super.binding as ItemInvalidMailboxBinding
+
+ fun displayInvalidMailbox(
+ mailbox: Mailbox,
+ onLockedMailboxClicked: (String) -> Unit,
+ onInvalidPasswordMailboxClicked: (Mailbox) -> Unit,
+ ) = with(binding.root) {
+ SentryLog.d("Bind", "Bind Invalid Mailbox (${mailbox.email})")
+
+ text = mailbox.email
+ itemStyle = DecoratedItemView.SelectionStyle.MENU_DRAWER
+ isPasswordOutdated = !mailbox.isPasswordValid
+ isMailboxLocked = mailbox.isLocked
+ hasNoValidMailboxes = false
+
+ computeEndIconVisibility()
+
+ initSetOnClickListener(
+ onLockedMailboxClicked = { onLockedMailboxClicked(mailbox.email) },
+ onInvalidPasswordMailboxClicked = { onInvalidPasswordMailboxClicked(mailbox) },
+ )
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/MailboxViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/MailboxViewHolder.kt
new file mode 100644
index 0000000000..d53c1c9e4d
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/MailboxViewHolder.kt
@@ -0,0 +1,51 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.infomaniak.lib.core.utils.SentryLog
+import com.infomaniak.mail.MatomoMail
+import com.infomaniak.mail.MatomoMail.trackMenuDrawerEvent
+import com.infomaniak.mail.data.models.mailbox.Mailbox
+import com.infomaniak.mail.databinding.ItemMenuDrawerMailboxBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+
+class MailboxViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerMailboxBinding.inflate(inflater, parent, false)) {
+
+ override val binding = super.binding as ItemMenuDrawerMailboxBinding
+
+ fun displayMailbox(
+ mailbox: Mailbox,
+ onValidMailboxClicked: (Int) -> Unit,
+ ) = with(binding.root) {
+ SentryLog.d("Bind", "Bind Mailbox (${mailbox.email})")
+
+ text = mailbox.email
+ unreadCount = mailbox.unreadCountDisplay.count
+ isPastilleDisplayed = mailbox.unreadCountDisplay.shouldDisplayPastille
+
+ setOnClickListener {
+ context.trackMenuDrawerEvent(MatomoMail.SWITCH_MAILBOX_NAME)
+ onValidMailboxClicked(mailbox.mailboxId)
+ }
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/MailboxesHeaderViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/MailboxesHeaderViewHolder.kt
new file mode 100644
index 0000000000..8a869468cd
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/MailboxesHeaderViewHolder.kt
@@ -0,0 +1,71 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.menuDrawer.items
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import com.infomaniak.lib.core.utils.SentryLog
+import com.infomaniak.mail.R
+import com.infomaniak.mail.data.models.mailbox.Mailbox
+import com.infomaniak.mail.databinding.ItemMenuDrawerMailboxesHeaderBinding
+import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerAdapter.MenuDrawerViewHolder
+import com.infomaniak.mail.utils.extensions.toggleChevron
+
+class MailboxesHeaderViewHolder(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+) : MenuDrawerViewHolder(ItemMenuDrawerMailboxesHeaderBinding.inflate(inflater, parent, false)) {
+
+ override val binding = super.binding as ItemMenuDrawerMailboxesHeaderBinding
+
+ fun displayMailboxesHeader(
+ header: MailboxesHeader,
+ onMailboxesHeaderClicked: () -> Unit,
+ ) = with(binding) {
+ SentryLog.d("Bind", "Bind Mailboxes header")
+
+ val (mailbox, hasMoreThanOneMailbox, isExpanded) = header
+
+ root.apply {
+ isClickable = hasMoreThanOneMailbox
+ isFocusable = hasMoreThanOneMailbox
+ setOnClickListener { onMailboxesHeaderClicked() }
+ }
+
+ mailboxSwitcherText.text = mailbox?.email
+ setMailboxSwitcherTextAppearance(isExpanded)
+
+ mailboxExpandButton.isVisible = hasMoreThanOneMailbox
+ }
+
+ fun updateCollapseState(
+ header: MailboxesHeader,
+ ) = with(binding) {
+ SentryLog.d("Bind", "Bind Mailboxes header because of collapse change")
+
+ mailboxExpandButton.toggleChevron(!header.isExpanded)
+ setMailboxSwitcherTextAppearance(header.isExpanded)
+ }
+
+ private fun ItemMenuDrawerMailboxesHeaderBinding.setMailboxSwitcherTextAppearance(isOpen: Boolean) {
+ mailboxSwitcherText.setTextAppearance(if (isOpen) R.style.BodyMedium_Accent else R.style.BodyMedium)
+ }
+
+ data class MailboxesHeader(val mailbox: Mailbox?, val hasMoreThanOneMailbox: Boolean, val isExpanded: Boolean)
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/move/MoveAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/move/MoveAdapter.kt
new file mode 100644
index 0000000000..0c567461fb
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/move/MoveAdapter.kt
@@ -0,0 +1,137 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2022-2024 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.infomaniak.mail.ui.main.move
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.viewbinding.ViewBinding
+import com.infomaniak.mail.R
+import com.infomaniak.mail.data.models.Folder
+import com.infomaniak.mail.databinding.ItemDividerHorizontalBinding
+import com.infomaniak.mail.databinding.ItemSelectableFolderBinding
+import com.infomaniak.mail.ui.main.move.MoveAdapter.FolderViewHolder
+import com.infomaniak.mail.utils.Utils.runCatchingRealm
+import com.infomaniak.mail.views.itemViews.SelectableFolderItemView
+import com.infomaniak.mail.views.itemViews.setFolderUi
+import javax.inject.Inject
+
+class MoveAdapter @Inject constructor() : ListAdapter(FolderDiffCallback()) {
+
+ private var selectedFolderId: String? = null
+
+ private lateinit var onFolderClicked: (folderId: String) -> Unit
+ private var onCollapseClicked: ((folderId: String, shouldCollapse: Boolean) -> Unit)? = null
+
+ operator fun invoke(
+ onFolderClicked: (folderId: String) -> Unit,
+ onCollapseClicked: ((folderId: String, shouldCollapse: Boolean) -> Unit)? = null,
+ ): MoveAdapter {
+ this.onFolderClicked = onFolderClicked
+ this.onCollapseClicked = onCollapseClicked
+
+ return this
+ }
+
+ fun setFolders(newSelectedFolderId: String, newFolders: List) = runCatchingRealm {
+ selectedFolderId = newSelectedFolderId
+ submitList(newFolders)
+ }
+
+ override fun getItemCount(): Int = runCatchingRealm { currentList.size }.getOrDefault(0)
+
+ override fun getItemViewType(position: Int): Int = runCatchingRealm {
+ return when (currentList[position]) {
+ is Folder -> DisplayType.FOLDER.layout
+ else -> DisplayType.DIVIDER.layout
+ }
+ }.getOrDefault(super.getItemViewType(position))
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
+ val layoutInflater = LayoutInflater.from(parent.context)
+ val binding = if (viewType == DisplayType.FOLDER.layout) {
+ ItemSelectableFolderBinding.inflate(layoutInflater, parent, false)
+ } else {
+ ItemDividerHorizontalBinding.inflate(layoutInflater, parent, false)
+ }
+ return FolderViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: FolderViewHolder, position: Int, payloads: MutableList) = runCatchingRealm {
+ if (payloads.firstOrNull() == Unit) {
+ val isSelected = selectedFolderId == (currentList[position] as Folder).id
+ (holder.binding as ItemSelectableFolderBinding).root.setSelectedState(isSelected)
+ } else {
+ super.onBindViewHolder(holder, position, payloads)
+ }
+ }.getOrDefault(Unit)
+
+ override fun onBindViewHolder(holder: FolderViewHolder, position: Int) = with(holder.binding) {
+ if (getItemViewType(position) == DisplayType.FOLDER.layout) {
+ (this as ItemSelectableFolderBinding).root.displayFolder(currentList[position] as Folder)
+ }
+ }
+
+ private fun SelectableFolderItemView.displayFolder(folder: Folder) {
+
+ val isSelected = folder.id == selectedFolderId
+
+ folder.role?.let {
+ setFolderUi(folder, it.folderIconRes, isSelected)
+ } ?: run {
+ setFolderUi(
+ folder = folder,
+ iconId = if (folder.isFavorite) R.drawable.ic_folder_star else R.drawable.ic_folder,
+ isSelected = isSelected,
+ )
+ }
+
+ setOnClickListener { onFolderClicked.invoke(folder.id) }
+ }
+
+ class FolderViewHolder(val binding: ViewBinding) : ViewHolder(binding.root)
+
+ private enum class DisplayType(val layout: Int) {
+ FOLDER(R.layout.item_selectable_folder),
+ DIVIDER(R.layout.item_divider_horizontal),
+ }
+
+ private class FolderDiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: Any, newItem: Any) = runCatchingRealm {
+ return when {
+ oldItem is Unit && newItem is Unit -> true // Unit is Divider item. They don't have any content, so always true.
+ oldItem is Folder && newItem is Folder && oldItem.id == newItem.id -> true
+ else -> false
+ }
+ }.getOrDefault(false)
+
+ override fun areContentsTheSame(oldFolder: Any, newFolder: Any) = runCatchingRealm {
+ oldFolder is Folder && newFolder is Folder &&
+ oldFolder.name == newFolder.name &&
+ oldFolder.isFavorite == newFolder.isFavorite &&
+ oldFolder.path == newFolder.path &&
+ oldFolder.unreadCountDisplay == newFolder.unreadCountDisplay &&
+ oldFolder.threads.count() == newFolder.threads.count() &&
+ oldFolder.isHidden == newFolder.isHidden &&
+ oldFolder.canBeCollapsed == newFolder.canBeCollapsed
+ }.getOrDefault(false)
+ }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MoveFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/move/MoveFragment.kt
similarity index 51%
rename from app/src/main/java/com/infomaniak/mail/ui/main/menu/MoveFragment.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/move/MoveFragment.kt
index 94aeee2201..0e84f4cf9c 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MoveFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/move/MoveFragment.kt
@@ -15,19 +15,18 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.infomaniak.mail.ui.main.menu
+package com.infomaniak.mail.ui.main.move
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.core.view.isGone
-import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
-import androidx.recyclerview.widget.RecyclerView
import com.infomaniak.lib.core.utils.hideKeyboard
import com.infomaniak.lib.core.utils.safeBinding
import com.infomaniak.mail.MatomoMail.SEARCH_DELETE_NAME
@@ -35,9 +34,11 @@ import com.infomaniak.mail.MatomoMail.SEARCH_VALIDATE_NAME
import com.infomaniak.mail.MatomoMail.trackCreateFolderEvent
import com.infomaniak.mail.MatomoMail.trackMoveSearchEvent
import com.infomaniak.mail.R
-import com.infomaniak.mail.data.models.Folder
-import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.databinding.FragmentMoveBinding
+import com.infomaniak.mail.ui.MainViewModel
+import com.infomaniak.mail.ui.alertDialogs.CreateFolderDialog
+import com.infomaniak.mail.utils.Utils
+import com.infomaniak.mail.utils.extensions.bindAlertToViewLifecycle
import com.infomaniak.mail.utils.extensions.handleEditorSearchAction
import com.infomaniak.mail.utils.extensions.setOnClearTextClickListener
import com.infomaniak.mail.utils.extensions.setSystemBarsColors
@@ -45,27 +46,18 @@ import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
-class MoveFragment : MenuFoldersFragment() {
+class MoveFragment : Fragment() {
private var binding: FragmentMoveBinding by safeBinding()
private val navigationArgs: MoveFragmentArgs by navArgs()
private val moveViewModel: MoveViewModel by viewModels()
+ private val mainViewModel: MainViewModel by activityViewModels()
@Inject
- lateinit var searchFolderAdapter: FolderAdapter
+ lateinit var createFolderDialog: CreateFolderDialog
- private val searchResultsAdapter by lazy {
- searchFolderAdapter(isInMenuDrawer, shouldIndent = false, onFolderClicked = ::onFolderSelected)
- }
-
- private var hasAlreadyTrackedFolderSearch = false
-
- private var currentFolderId: String? = null
-
- override val defaultFoldersList: RecyclerView by lazy { binding.defaultFoldersList }
- override val customFoldersList: RecyclerView by lazy { binding.customFoldersList }
-
- override val isInMenuDrawer = false
+ @Inject
+ lateinit var moveAdapter: MoveAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return FragmentMoveBinding.inflate(inflater, container, false).also { binding = it }.root
@@ -75,104 +67,80 @@ class MoveFragment : MenuFoldersFragment() {
super.onViewCreated(view, savedInstanceState)
setSystemBarsColors()
+ bindAlertToViewLifecycle(createFolderDialog)
+ setupRecyclerView()
setupListeners()
- setupFolderAdapters()
setupCreateFolderDialog()
- observeNewFolderCreation()
+ setupSearchBar()
observeSearchResults()
+ observeFolderCreation()
}
- override fun onStop() {
- binding.searchTextInput.hideKeyboard()
- moveViewModel.cancelSearch()
- super.onStop()
+ private fun setupRecyclerView() = with(binding.foldersRecyclerView) {
+ adapter = moveAdapter(onFolderClicked = ::onFolderSelected)
}
private fun setupListeners() = with(binding) {
toolbar.setNavigationOnClickListener { findNavController().popBackStack() }
iconAddFolder.setOnClickListener {
trackCreateFolderEvent("fromMove")
- inputDialog.show(
- title = R.string.newFolderDialogTitle,
- hint = R.string.newFolderDialogHint,
- confirmButtonText = R.string.newFolderDialogMovePositiveButton,
- )
+ createFolderDialog.show(confirmButtonText = R.string.newFolderDialogMovePositiveButton)
}
}
- private fun setupCreateFolderDialog() {
- inputDialog.setCallbacks(
+ private fun setupCreateFolderDialog() = with(navigationArgs) {
+ createFolderDialog.setCallbacks(
onPositiveButtonClicked = { folderName ->
- trackCreateFolderEvent("confirm")
- mainViewModel.moveToNewFolder(folderName, navigationArgs.threadsUids.toList(), navigationArgs.messageUid)
+ mainViewModel.moveToNewFolder(folderName, threadsUids.toList(), messageUid)
},
- onErrorCheck = ::checkForFolderCreationErrors,
)
}
- private fun setupFolderAdapters() {
- moveViewModel.getFolderIdAndCustomFolders().observe(viewLifecycleOwner) { (folderId, customFolders) ->
+ private fun observeSearchResults() = with(moveViewModel) {
+ Utils.waitInitMediator(sourceFolderIdLiveData, filterResults).observe(viewLifecycleOwner) { (sourceFolderId, folders) ->
+ moveAdapter.setFolders(sourceFolderId, folders)
+ }
+ }
- currentFolderId = folderId
+ private fun observeFolderCreation() = with(mainViewModel) {
- val defaultFoldersWithoutDraft = mainViewModel.currentDefaultFoldersLive.value!!.let { folders ->
- folders.filterNot { it.role == FolderRole.DRAFT }
- }
-
- defaultFoldersAdapter.setFolders(defaultFoldersWithoutDraft, folderId)
- customFoldersAdapter.setFolders(customFolders, folderId)
- setSearchBarUi(allFolders = defaultFoldersWithoutDraft + customFolders)
+ newFolderResultTrigger.observe(viewLifecycleOwner) {
+ createFolderDialog.resetLoadingAndDismiss()
}
- }
- private fun observeNewFolderCreation() = with(mainViewModel) {
- newFolderResultTrigger.observe(viewLifecycleOwner) { inputDialog.resetLoadingAndDismiss() }
isMovedToNewFolder.observe(viewLifecycleOwner) { isFolderCreated ->
if (isFolderCreated) findNavController().popBackStack()
}
}
- override fun onFolderSelected(folderId: String): Unit = with(navigationArgs) {
+ private fun onFolderSelected(folderId: String): Unit = with(navigationArgs) {
mainViewModel.moveThreadsOrMessageTo(folderId, threadsUids.toList(), messageUid)
findNavController().popBackStack()
}
- override fun onFolderCollapse(folderId: String, shouldCollapse: Boolean) = Unit
-
- private fun setSearchBarUi(allFolders: List) = with(binding) {
- searchResultsList.adapter = searchResultsAdapter
+ private fun setupSearchBar() = with(binding) {
searchInputLayout.setOnClearTextClickListener { trackMoveSearchEvent(SEARCH_DELETE_NAME) }
searchTextInput.apply {
- toggleFolderListsVisibility(!text.isNullOrBlank())
doOnTextChanged { newQuery, _, _, _ ->
- toggleFolderListsVisibility(!newQuery.isNullOrBlank())
- if (newQuery?.isNotBlank() == true) {
- moveViewModel.filterFolders(newQuery.toString(), allFolders, shouldDebounce = true)
- }
-
- if (!hasAlreadyTrackedFolderSearch) {
+ moveViewModel.filterFolders(newQuery, shouldDebounce = true)
+ if (!moveViewModel.hasAlreadyTrackedSearch) {
trackMoveSearchEvent("executeSearch")
- hasAlreadyTrackedFolderSearch = true
+ moveViewModel.hasAlreadyTrackedSearch = true
}
}
handleEditorSearchAction { query ->
- moveViewModel.filterFolders(query, allFolders, shouldDebounce = false)
+ moveViewModel.filterFolders(query, shouldDebounce = false)
trackMoveSearchEvent(SEARCH_VALIDATE_NAME)
}
}
}
- private fun toggleFolderListsVisibility(isSearching: Boolean) = with(binding) {
- searchResultsList.isVisible = isSearching
- moveFoldersLists.isGone = isSearching
- }
-
- private fun observeSearchResults() {
- moveViewModel.filterResults.observe(viewLifecycleOwner) { folders ->
- searchResultsAdapter.setFolders(folders, currentFolderId)
- }
+ override fun onStop() {
+ binding.searchTextInput.hideKeyboard()
+ moveViewModel.cancelSearch()
+ super.onStop()
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MoveViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/move/MoveViewModel.kt
similarity index 59%
rename from app/src/main/java/com/infomaniak/mail/ui/main/menu/MoveViewModel.kt
rename to app/src/main/java/com/infomaniak/mail/ui/main/move/MoveViewModel.kt
index 39ec1c557e..76b0698290 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/menu/MoveViewModel.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/move/MoveViewModel.kt
@@ -15,13 +15,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.infomaniak.mail.ui.main.menu
+package com.infomaniak.mail.ui.main.move
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
-import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.infomaniak.mail.data.cache.mailboxContent.FolderController
import com.infomaniak.mail.data.cache.mailboxContent.MessageController
@@ -29,8 +28,9 @@ import com.infomaniak.mail.data.cache.mailboxContent.ThreadController
import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.di.IoDispatcher
import com.infomaniak.mail.utils.coroutineContext
+import com.infomaniak.mail.utils.extensions.addDividerBeforeFirstCustomFolder
import com.infomaniak.mail.utils.extensions.appContext
-import com.infomaniak.mail.utils.extensions.getCustomMenuFolders
+import com.infomaniak.mail.utils.extensions.flattenFolderChildren
import com.infomaniak.mail.utils.extensions.standardize
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
@@ -52,43 +52,69 @@ class MoveViewModel @Inject constructor(
private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher)
- private var filterJob: Job? = null
+ private var searchJob: Job? = null
private val messageUid inline get() = savedStateHandle.get(MoveFragmentArgs::messageUid.name)
private val threadsUids inline get() = savedStateHandle.get>(MoveFragmentArgs::threadsUids.name)!!
- var filterResults: MutableLiveData> = MutableLiveData()
+ private var allFolders = emptyList()
+ val sourceFolderIdLiveData = MutableLiveData()
+ val filterResults = MutableLiveData>()
+ var hasAlreadyTrackedSearch = false
- fun cancelSearch() {
- filterJob?.cancel()
- }
+ init {
+ viewModelScope.launch(ioCoroutineContext) {
- fun getFolderIdAndCustomFolders() = liveData(ioCoroutineContext) {
+ val sourceFolderId = messageUid?.let(messageController::getMessage)?.folderId
+ ?: threadController.getThread(threadsUids.first())!!.folderId
- val folderId = messageUid?.let { messageController.getMessage(it)!!.folderId }
- ?: threadController.getThread(threadsUids.first())!!.folderId
+ sourceFolderIdLiveData.postValue(sourceFolderId)
- val customFolders = folderController.getCustomFolders().getCustomMenuFolders()
+ allFolders = folderController.getMoveFolders()
+ .flattenFolderChildren()
+ .addDividerBeforeFirstCustomFolder(dividerType = Unit)
+ .also(filterResults::postValue)
+ }
+ }
- emit(folderId to customFolders)
+ fun filterFolders(query: CharSequence?, shouldDebounce: Boolean) {
+ if (query?.isNotBlank() == true) {
+ searchFolders(query, shouldDebounce)
+ } else {
+ cancelSearch()
+ filterResults.value = allFolders
+ }
}
- fun filterFolders(query: String, folders: List, shouldDebounce: Boolean) = viewModelScope.launch(ioCoroutineContext) {
- filterJob?.cancel()
- filterJob = launch {
+ private fun searchFolders(query: CharSequence, shouldDebounce: Boolean) = viewModelScope.launch(ioCoroutineContext) {
+
+ cancelSearch()
+
+ searchJob = launch {
+
if (shouldDebounce) {
delay(FILTER_DEBOUNCE_DURATION)
ensureActive()
}
- val filteredFolders = folders.filter { folder ->
- val folderName = folder.role?.folderNameRes?.let(appContext::getString) ?: folder.name
- folderName.standardize().contains(query.standardize())
+
+ val filteredFolders = mutableListOf().apply {
+ allFolders.forEach { folder ->
+ ensureActive()
+ if (folder !is Folder) return@forEach
+ val folderName = folder.role?.folderNameRes?.let(appContext::getString) ?: folder.name
+ val isFound = folderName.standardize().contains(query.standardize())
+ if (isFound) add(folder)
+ }
}
filterResults.postValue(filteredFolders)
}
}
+ fun cancelSearch() {
+ searchJob?.cancel()
+ }
+
override fun onCleared() {
cancelSearch()
super.onCleared()
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt
index d463621de5..c9e10a0d83 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt
@@ -51,12 +51,13 @@ import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.data.models.thread.Thread.ThreadFilter
import com.infomaniak.mail.databinding.FragmentSearchBinding
-import com.infomaniak.mail.ui.main.folder.ThreadListAdapterCallback
+import com.infomaniak.mail.ui.main.folder.ThreadListAdapterCallbacks
import com.infomaniak.mail.ui.main.folder.TwoPaneFragment
import com.infomaniak.mail.ui.main.search.SearchFolderAdapter.SearchFolderElement
import com.infomaniak.mail.ui.main.thread.ThreadFragment
import com.infomaniak.mail.utils.RealmChangesBinding.Companion.bindResultsChangeToAdapter
import com.infomaniak.mail.utils.Utils.Shortcuts
+import com.infomaniak.mail.utils.extensions.addDividerBeforeFirstCustomFolder
import com.infomaniak.mail.utils.extensions.addStickyDateDecoration
import com.infomaniak.mail.utils.extensions.getLocalizedNameOrAllFolders
import com.infomaniak.mail.utils.extensions.handleEditorSearchAction
@@ -153,7 +154,7 @@ class SearchFragment : TwoPaneFragment() {
threadListAdapter(
folderRole = null,
isFolderNameVisible = true,
- threadListAdapterCallback = object : ThreadListAdapterCallback {
+ callbacks = object : ThreadListAdapterCallbacks {
override var onSwipeFinished: (() -> Unit)? = null
@@ -202,13 +203,13 @@ class SearchFragment : TwoPaneFragment() {
width = resources.getDimensionPixelSize(R.dimen.maxSearchChipWidth)
}
- searchViewModel.foldersLive.observe(viewLifecycleOwner) { (defaultFolders, customFolders) ->
+ searchViewModel.foldersLive.observe(viewLifecycleOwner) { allFolders ->
- val folders = defaultFolders.toMutableList().apply {
- add(0, SearchFolderElement.ALL_FOLDERS)
- add(SearchFolderElement.DIVIDER)
- addAll(customFolders)
- }.toList()
+ val folders = allFolders
+ .addDividerBeforeFirstCustomFolder(dividerType = SearchFolderElement.DIVIDER)
+ .toMutableList()
+ .apply { add(0, SearchFolderElement.ALL_FOLDERS) }
+ .toList()
searchAdapter = SearchFolderAdapter(folders) { folder, title ->
onFolderSelected(folder, title)
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchViewModel.kt
index 4c775e4969..0836f94d7d 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchViewModel.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchViewModel.kt
@@ -42,7 +42,7 @@ import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.SearchUtils
import com.infomaniak.mail.utils.coroutineContext
import com.infomaniak.mail.utils.extensions.appContext
-import com.infomaniak.mail.utils.extensions.getMenuFolders
+import com.infomaniak.mail.utils.extensions.flattenFolderChildren
import dagger.hilt.android.lifecycle.HiltViewModel
import io.sentry.Sentry
import kotlinx.coroutines.CoroutineDispatcher
@@ -79,8 +79,8 @@ class SearchViewModel @Inject constructor(
var currentSearchQuery: String = ""
private set
- val foldersLive = folderController.getRootsFoldersAsync()
- .map { it.list.getMenuFolders() }
+ val foldersLive = folderController.getSearchFoldersAsync()
+ .map { it.list.flattenFolderChildren() }
.asLiveData(ioCoroutineContext)
private var currentFilters = mutableSetOf()
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/AccentColorSettingFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/AccentColorSettingFragment.kt
index 2ef8d65cd3..afe1750880 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/AccentColorSettingFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/AccentColorSettingFragment.kt
@@ -31,7 +31,9 @@ import com.infomaniak.mail.MatomoMail.trackEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.LocalSettings.AccentColor
-import com.infomaniak.mail.data.LocalSettings.AccentColor.*
+import com.infomaniak.mail.data.LocalSettings.AccentColor.BLUE
+import com.infomaniak.mail.data.LocalSettings.AccentColor.PINK
+import com.infomaniak.mail.data.LocalSettings.AccentColor.SYSTEM
import com.infomaniak.mail.databinding.FragmentAccentColorSettingBinding
import com.infomaniak.mail.utils.extensions.setSystemBarsColors
import dagger.hilt.android.AndroidEntryPoint
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/ThemeSettingFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/ThemeSettingFragment.kt
index 990b60bf64..5ba939f37a 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/ThemeSettingFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/ThemeSettingFragment.kt
@@ -30,7 +30,9 @@ import com.infomaniak.mail.MatomoMail.trackEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.LocalSettings.Theme
-import com.infomaniak.mail.data.LocalSettings.Theme.*
+import com.infomaniak.mail.data.LocalSettings.Theme.DARK
+import com.infomaniak.mail.data.LocalSettings.Theme.LIGHT
+import com.infomaniak.mail.data.LocalSettings.Theme.SYSTEM
import com.infomaniak.mail.databinding.FragmentThemeSettingBinding
import com.infomaniak.mail.utils.extensions.setSystemBarsColors
import dagger.hilt.android.AndroidEntryPoint
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/ThreadListDensitySettingFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/ThreadListDensitySettingFragment.kt
index c1e199ee00..c566696224 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/ThreadListDensitySettingFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/ThreadListDensitySettingFragment.kt
@@ -26,7 +26,9 @@ import com.infomaniak.lib.core.utils.safeBinding
import com.infomaniak.mail.MatomoMail.trackEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
-import com.infomaniak.mail.data.LocalSettings.ThreadDensity.*
+import com.infomaniak.mail.data.LocalSettings.ThreadDensity.COMPACT
+import com.infomaniak.mail.data.LocalSettings.ThreadDensity.LARGE
+import com.infomaniak.mail.data.LocalSettings.ThreadDensity.NORMAL
import com.infomaniak.mail.databinding.FragmentThreadListDensitySettingBinding
import com.infomaniak.mail.utils.extensions.setSystemBarsColors
import dagger.hilt.android.AndroidEntryPoint
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/swipe/SwipeActionsSelectionSettingFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/swipe/SwipeActionsSelectionSettingFragment.kt
index 152078ff43..d9b6492419 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/swipe/SwipeActionsSelectionSettingFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/swipe/SwipeActionsSelectionSettingFragment.kt
@@ -30,7 +30,15 @@ import com.infomaniak.mail.MatomoMail.trackEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.LocalSettings.SwipeAction
-import com.infomaniak.mail.data.LocalSettings.SwipeAction.*
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.ARCHIVE
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.DELETE
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.FAVORITE
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.MOVE
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.NONE
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.POSTPONE
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.QUICKACTIONS_MENU
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.READ_UNREAD
+import com.infomaniak.mail.data.LocalSettings.SwipeAction.SPAM
import com.infomaniak.mail.databinding.FragmentSwipeActionsSelectionSettingBinding
import com.infomaniak.mail.utils.extensions.setSystemBarsColors
import dagger.hilt.android.AndroidEntryPoint
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt
index 24ec4eb3bb..41e8190ea4 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt
@@ -60,6 +60,7 @@ import com.infomaniak.mail.utils.MailDateFormatUtils.mailFormattedDate
import com.infomaniak.mail.utils.MailDateFormatUtils.mostDetailedDate
import com.infomaniak.mail.utils.MessageBodyUtils
import com.infomaniak.mail.utils.SharedUtils.Companion.createHtmlForPlainText
+import com.infomaniak.mail.utils.UiUtils
import com.infomaniak.mail.utils.UiUtils.getPrettyNameAndEmail
import com.infomaniak.mail.utils.Utils
import com.infomaniak.mail.utils.Utils.TEXT_HTML
@@ -90,7 +91,7 @@ class ThreadAdapter(
private var threadAdapterCallbacks: ThreadAdapterCallbacks? = null,
) : ListAdapter(MessageDiffCallback()) {
- inline val items: MutableList get() = currentList
+ inline val items: List get() = currentList
//region Auto-scroll at Thread opening
private val currentSetOfLoadedExpandedMessagesUids = mutableSetOf()
@@ -183,7 +184,7 @@ class ThreadAdapter(
val item = items[position]
holder.binding.root.tag = if (item is SuperCollapsedBlock || (item is Message && item.shouldHideDivider)) {
- IGNORE_DIVIDER_TAG
+ UiUtils.IGNORE_DIVIDER_TAG
} else {
null
}
@@ -851,9 +852,6 @@ class ThreadAdapter(
}
companion object {
-
- const val IGNORE_DIVIDER_TAG = "ignoreDividerTag"
-
private val contextMenuTypeForHitTestResultType = mapOf(
HitTestResult.PHONE_TYPE to ContextMenuType.PHONE,
HitTestResult.EMAIL_TYPE to ContextMenuType.EMAIL,
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt
index debff28e89..2082a13418 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt
@@ -81,6 +81,7 @@ import com.infomaniak.mail.ui.main.thread.actions.ReplyBottomSheetDialogArgs
import com.infomaniak.mail.ui.main.thread.actions.ThreadActionsBottomSheetDialogArgs
import com.infomaniak.mail.ui.main.thread.calendar.AttendeesBottomSheetDialogArgs
import com.infomaniak.mail.utils.PermissionUtils
+import com.infomaniak.mail.utils.UiUtils
import com.infomaniak.mail.utils.UiUtils.dividerDrawable
import com.infomaniak.mail.utils.extensions.AttachmentExtensions.openAttachment
import com.infomaniak.mail.utils.extensions.bindAlertToViewLifecycle
@@ -324,7 +325,7 @@ class ThreadFragment : Fragment() {
binding.messagesList.addItemDecoration(
DividerItemDecorator(
divider = InsetDrawable(dividerDrawable(requireContext()), 0),
- shouldIgnoreView = { view -> view.tag == ThreadAdapter.IGNORE_DIVIDER_TAG },
+ shouldIgnoreView = { view -> view.tag == UiUtils.IGNORE_DIVIDER_TAG },
),
)
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt
index b47103b2ff..156a4ec8cf 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt
@@ -40,7 +40,7 @@ import com.infomaniak.mail.R
import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.draft.Draft.DraftMode
import com.infomaniak.mail.ui.alertDialogs.DescriptionAlertDialog
-import com.infomaniak.mail.ui.main.menu.MoveFragmentArgs
+import com.infomaniak.mail.ui.main.move.MoveFragmentArgs
import com.infomaniak.mail.ui.main.thread.PrintMailFragmentArgs
import com.infomaniak.mail.utils.extensions.animatedNavigation
import com.infomaniak.mail.utils.extensions.deleteWithConfirmationPopup
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MultiSelectBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MultiSelectBottomSheetDialog.kt
index d6aaac98f8..27d3f265c7 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MultiSelectBottomSheetDialog.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MultiSelectBottomSheetDialog.kt
@@ -72,7 +72,7 @@ class MultiSelectBottomSheetDialog : ActionsBottomSheetDialog() {
R.id.actionMove -> {
trackMultiSelectActionEvent(ACTION_MOVE_NAME, selectedThreadsCount, isFromBottomSheet = true)
animatedNavigation(
- ThreadListFragmentDirections.actionThreadListFragmentToMoveFragment(
+ directions = ThreadListFragmentDirections.actionThreadListFragmentToMoveFragment(
threadsUids = selectedThreadsUids.toTypedArray(),
),
currentClassName = currentClassName,
@@ -98,6 +98,7 @@ class MultiSelectBottomSheetDialog : ActionsBottomSheetDialog() {
}
}
+ // TODO
// binding.postpone.setClosingOnClickListener {
// trackMultiSelectActionEvent(ACTION_POSTPONE_NAME, selectedThreadsCount, isFromBottomSheet = true)
// notYetImplemented()
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ThreadActionsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ThreadActionsBottomSheetDialog.kt
index 65ef690547..4fea86e56e 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ThreadActionsBottomSheetDialog.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ThreadActionsBottomSheetDialog.kt
@@ -43,7 +43,7 @@ import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.draft.Draft.DraftMode
import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.ui.alertDialogs.DescriptionAlertDialog
-import com.infomaniak.mail.ui.main.menu.MoveFragmentArgs
+import com.infomaniak.mail.ui.main.move.MoveFragmentArgs
import com.infomaniak.mail.utils.extensions.animatedNavigation
import com.infomaniak.mail.utils.extensions.deleteWithConfirmationPopup
import com.infomaniak.mail.utils.extensions.notYetImplemented
@@ -106,102 +106,104 @@ class ThreadActionsBottomSheetDialog : MailActionsBottomSheetDialog() {
}
private fun setupListeners(thread: Thread, messageUidToReply: String) = with(navigationArgs) {
- initOnClickListener(object : OnActionClick {
- //region Main actions
- override fun onReply() {
- trackBottomSheetThreadActionsEvent(ACTION_REPLY_NAME)
- safeNavigateToNewMessageActivity(
- draftMode = DraftMode.REPLY,
- previousMessageUid = messageUidToReply,
- currentClassName = currentClassName,
- shouldLoadDistantResources = shouldLoadDistantResources,
- )
- }
-
- override fun onReplyAll() {
- trackBottomSheetThreadActionsEvent(ACTION_REPLY_ALL_NAME)
- safeNavigateToNewMessageActivity(
- draftMode = DraftMode.REPLY_ALL,
- previousMessageUid = messageUidToReply,
- currentClassName = currentClassName,
- shouldLoadDistantResources = shouldLoadDistantResources,
- )
- }
-
- override fun onForward() {
- trackBottomSheetThreadActionsEvent(ACTION_FORWARD_NAME)
- safeNavigateToNewMessageActivity(
- draftMode = DraftMode.FORWARD,
- previousMessageUid = messageUidToReply,
- currentClassName = currentClassName,
- shouldLoadDistantResources = shouldLoadDistantResources,
- )
- }
-
- override fun onDelete() {
- descriptionDialog.deleteWithConfirmationPopup(folderRole, count = 1) {
- trackBottomSheetThreadActionsEvent(ACTION_DELETE_NAME)
- mainViewModel.deleteThread(threadUid)
+ initOnClickListener(
+ listener = object : OnActionClick {
+ //region Main actions
+ override fun onReply() {
+ trackBottomSheetThreadActionsEvent(ACTION_REPLY_NAME)
+ safeNavigateToNewMessageActivity(
+ draftMode = DraftMode.REPLY,
+ previousMessageUid = messageUidToReply,
+ currentClassName = currentClassName,
+ shouldLoadDistantResources = shouldLoadDistantResources,
+ )
+ }
+
+ override fun onReplyAll() {
+ trackBottomSheetThreadActionsEvent(ACTION_REPLY_ALL_NAME)
+ safeNavigateToNewMessageActivity(
+ draftMode = DraftMode.REPLY_ALL,
+ previousMessageUid = messageUidToReply,
+ currentClassName = currentClassName,
+ shouldLoadDistantResources = shouldLoadDistantResources,
+ )
}
- }
- //endregion
-
- //region Actions
- override fun onArchive() = with(mainViewModel) {
- trackBottomSheetThreadActionsEvent(ACTION_ARCHIVE_NAME, isFromArchive)
- archiveThread(threadUid)
- }
-
- override fun onReadUnread() {
- trackBottomSheetThreadActionsEvent(ACTION_MARK_AS_SEEN_NAME, value = thread.unseenMessagesCount == 0)
- mainViewModel.toggleThreadSeenStatus(threadUid)
- twoPaneViewModel.closeThread()
- }
-
- override fun onMove() {
- trackBottomSheetThreadActionsEvent(ACTION_MOVE_NAME)
- animatedNavigation(R.id.moveFragment, MoveFragmentArgs(arrayOf(threadUid)).toBundle(), currentClassName)
- }
-
- override fun onPostpone() {
- trackBottomSheetThreadActionsEvent(ACTION_POSTPONE_NAME)
- notYetImplemented()
- }
-
- override fun onFavorite() {
- trackBottomSheetThreadActionsEvent(ACTION_FAVORITE_NAME, thread.isFavorite)
- mainViewModel.toggleThreadFavoriteStatus(threadUid)
- }
-
- override fun onReportJunk() {
- if (isFromSpam) {
- trackBottomSheetThreadActionsEvent(ACTION_SPAM_NAME, value = true)
- mainViewModel.toggleThreadSpamStatus(threadUid)
- } else {
- safeNavigate(
- resId = R.id.junkBottomSheetDialog,
- args = JunkBottomSheetDialogArgs(threadUid, messageUidToReply).toBundle(),
+
+ override fun onForward() {
+ trackBottomSheetThreadActionsEvent(ACTION_FORWARD_NAME)
+ safeNavigateToNewMessageActivity(
+ draftMode = DraftMode.FORWARD,
+ previousMessageUid = messageUidToReply,
currentClassName = currentClassName,
+ shouldLoadDistantResources = shouldLoadDistantResources,
)
}
- }
- override fun onPrint() {
- trackBottomSheetThreadActionsEvent(ACTION_PRINT_NAME)
- notYetImplemented()
- }
+ override fun onDelete() {
+ descriptionDialog.deleteWithConfirmationPopup(folderRole, count = 1) {
+ trackBottomSheetThreadActionsEvent(ACTION_DELETE_NAME)
+ mainViewModel.deleteThread(threadUid)
+ }
+ }
+ //endregion
+
+ //region Actions
+ override fun onArchive() = with(mainViewModel) {
+ trackBottomSheetThreadActionsEvent(ACTION_ARCHIVE_NAME, isFromArchive)
+ archiveThread(threadUid)
+ }
+
+ override fun onReadUnread() {
+ trackBottomSheetThreadActionsEvent(ACTION_MARK_AS_SEEN_NAME, value = thread.unseenMessagesCount == 0)
+ mainViewModel.toggleThreadSeenStatus(threadUid)
+ twoPaneViewModel.closeThread()
+ }
- override fun onShare() {
- activity?.apply {
- trackBottomSheetThreadActionsEvent(ACTION_SHARE_LINK_NAME)
- mainViewModel.shareThreadUrl(messageUidToReply, ::shareString)
+ override fun onMove() {
+ trackBottomSheetThreadActionsEvent(ACTION_MOVE_NAME)
+ animatedNavigation(R.id.moveFragment, MoveFragmentArgs(arrayOf(threadUid)).toBundle(), currentClassName)
}
- }
- override fun onReportDisplayProblem() {
- notYetImplemented()
- }
- //endregion
- })
+ override fun onPostpone() {
+ trackBottomSheetThreadActionsEvent(ACTION_POSTPONE_NAME)
+ notYetImplemented()
+ }
+
+ override fun onFavorite() {
+ trackBottomSheetThreadActionsEvent(ACTION_FAVORITE_NAME, thread.isFavorite)
+ mainViewModel.toggleThreadFavoriteStatus(threadUid)
+ }
+
+ override fun onReportJunk() {
+ if (isFromSpam) {
+ trackBottomSheetThreadActionsEvent(ACTION_SPAM_NAME, value = true)
+ mainViewModel.toggleThreadSpamStatus(threadUid)
+ } else {
+ safeNavigate(
+ resId = R.id.junkBottomSheetDialog,
+ args = JunkBottomSheetDialogArgs(threadUid, messageUidToReply).toBundle(),
+ currentClassName = currentClassName,
+ )
+ }
+ }
+
+ override fun onPrint() {
+ trackBottomSheetThreadActionsEvent(ACTION_PRINT_NAME)
+ notYetImplemented()
+ }
+
+ override fun onShare() {
+ activity?.apply {
+ trackBottomSheetThreadActionsEvent(ACTION_SHARE_LINK_NAME)
+ mainViewModel.shareThreadUrl(messageUidToReply, ::shareString)
+ }
+ }
+
+ override fun onReportDisplayProblem() {
+ notYetImplemented()
+ }
+ //endregion
+ },
+ )
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/user/AccountFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/user/AccountFragment.kt
index 15a2cd77c6..30644c63a8 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/main/user/AccountFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/user/AccountFragment.kt
@@ -41,7 +41,7 @@ import com.infomaniak.mail.ui.MainActivity
import com.infomaniak.mail.ui.MainViewModel
import com.infomaniak.mail.ui.alertDialogs.DescriptionAlertDialog
import com.infomaniak.mail.ui.main.MailboxListFragment
-import com.infomaniak.mail.ui.main.menu.MailboxesAdapter
+import com.infomaniak.mail.ui.main.menuDrawer.MailboxesAdapter
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.ConfettiUtils
import com.infomaniak.mail.utils.ConfettiUtils.ConfettiType
diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/AiViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/AiViewModel.kt
index f77b956653..8ad90a8c1e 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/AiViewModel.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/AiViewModel.kt
@@ -36,7 +36,12 @@ import com.infomaniak.mail.data.models.ai.AssistantMessage
import com.infomaniak.mail.data.models.ai.ContextMessage
import com.infomaniak.mail.data.models.ai.UserMessage
import com.infomaniak.mail.di.IoDispatcher
-import com.infomaniak.mail.ui.newMessage.AiViewModel.PropositionStatus.*
+import com.infomaniak.mail.ui.newMessage.AiViewModel.PropositionStatus.CONTEXT_TOO_LONG
+import com.infomaniak.mail.ui.newMessage.AiViewModel.PropositionStatus.ERROR
+import com.infomaniak.mail.ui.newMessage.AiViewModel.PropositionStatus.MISSING_CONTENT
+import com.infomaniak.mail.ui.newMessage.AiViewModel.PropositionStatus.PROMPT_TOO_LONG
+import com.infomaniak.mail.ui.newMessage.AiViewModel.PropositionStatus.RATE_LIMIT_EXCEEDED
+import com.infomaniak.mail.ui.newMessage.AiViewModel.PropositionStatus.SUCCESS
import com.infomaniak.mail.utils.ErrorCode
import com.infomaniak.mail.utils.ErrorCode.MAX_SYNTAX_TOKENS_REACHED
import com.infomaniak.mail.utils.ErrorCode.TOO_MANY_REQUESTS
diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageManager.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageManager.kt
index 674057763f..24c4ad4336 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageManager.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageManager.kt
@@ -19,7 +19,6 @@ package com.infomaniak.mail.ui.newMessage
import androidx.lifecycle.Lifecycle.Event
import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleOwner
import com.infomaniak.mail.databinding.FragmentNewMessageBinding
abstract class NewMessageManager {
@@ -55,8 +54,10 @@ abstract class NewMessageManager {
}
private fun onFreeReferences(setReferencesToNull: () -> Unit) {
- viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _: LifecycleOwner, event: Event ->
- if (event == Event.ON_DESTROY) setReferencesToNull()
- })
+ viewLifecycleOwner.lifecycle.addObserver(
+ observer = LifecycleEventObserver { _, event ->
+ if (event == Event.ON_DESTROY) setReferencesToNull()
+ },
+ )
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt
index d63352a77c..18fdc512b6 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt
@@ -23,7 +23,9 @@ import androidx.core.view.isVisible
import com.infomaniak.mail.data.models.correspondent.Recipient
import com.infomaniak.mail.databinding.FragmentNewMessageBinding
import com.infomaniak.mail.ui.main.SnackbarManager
-import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldType.*
+import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldType.BCC
+import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldType.CC
+import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldType.TO
import com.infomaniak.mail.utils.extensions.copyRecipientEmailToClipboard
import dagger.hilt.android.scopes.FragmentScoped
import javax.inject.Inject
diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt
index 78389091fe..0d1caf9d5b 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt
@@ -71,7 +71,11 @@ import com.infomaniak.mail.ui.main.SnackbarManager
import com.infomaniak.mail.ui.newMessage.NewMessageActivity.DraftSaveConfiguration
import com.infomaniak.mail.ui.newMessage.NewMessageEditorManager.EditorAction
import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldType
-import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.SignatureScore.*
+import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.SignatureScore.EXACT_MATCH
+import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.SignatureScore.EXACT_MATCH_AND_IS_DEFAULT
+import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.SignatureScore.NO_MATCH
+import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.SignatureScore.ONLY_EMAIL_MATCH
+import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.SignatureScore.ONLY_EMAIL_MATCH_AND_IS_DEFAULT
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.ContactUtils.arrangeMergedContacts
import com.infomaniak.mail.utils.JsoupParserUtil.jsoupParseWithLog
@@ -366,9 +370,7 @@ class NewMessageViewModel @Inject constructor(
val prefix = when (draftMode) {
DraftMode.REPLY, DraftMode.REPLY_ALL -> if (subject.isReply()) "" else PREFIX_REPLY
DraftMode.FORWARD -> if (subject.isForward()) "" else PREFIX_FORWARD
- DraftMode.NEW_MAIL -> {
- throw IllegalStateException("`${DraftMode::class.simpleName}` cannot be `${DraftMode.NEW_MAIL.name}` here.")
- }
+ DraftMode.NEW_MAIL -> error("`${DraftMode::class.simpleName}` cannot be `${DraftMode.NEW_MAIL.name}` here.")
}
return prefix + subject
diff --git a/app/src/main/java/com/infomaniak/mail/ui/noValidMailboxes/NoValidMailboxesFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/noValidMailboxes/NoValidMailboxesFragment.kt
index 3f9b2e80e3..949dc0f4ca 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/noValidMailboxes/NoValidMailboxesFragment.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/noValidMailboxes/NoValidMailboxesFragment.kt
@@ -32,7 +32,7 @@ import com.infomaniak.mail.MatomoMail.trackNoValidMailboxesEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.databinding.FragmentNoValidMailboxesBinding
import com.infomaniak.mail.ui.main.MailboxListFragment
-import com.infomaniak.mail.ui.main.menu.MailboxesAdapter
+import com.infomaniak.mail.ui.main.menuDrawer.MailboxesAdapter
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@@ -55,7 +55,7 @@ class NoValidMailboxesFragment : Fragment(), MailboxListFragment {
return FragmentNoValidMailboxesBinding.inflate(inflater, container, false).also { binding = it }.root
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) {
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupAdapters()
diff --git a/app/src/main/java/com/infomaniak/mail/utils/JsoupParserUtil.kt b/app/src/main/java/com/infomaniak/mail/utils/JsoupParserUtil.kt
index 30e2af1bf6..046f59694d 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/JsoupParserUtil.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/JsoupParserUtil.kt
@@ -24,7 +24,7 @@ import org.jsoup.nodes.Document
object JsoupParserUtil {
private const val SENTRY_LOG_TAG = "Jsoup memory usage"
- private const val BYTE_TO_MEGABYTE_DIVIDER: Float = 1024f * 1024f
+ private const val BYTE_TO_MEGABYTE_DIVIDER = 1_024.0f * 1_024.0f
fun jsoupParseWithLog(value: String): Document {
return measureAndLogMemoryUsage(SENTRY_LOG_TAG, actionName = "parsing") {
diff --git a/app/src/main/java/com/infomaniak/mail/utils/NotificationUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/NotificationUtils.kt
index 66af7ce10a..1ea01a514b 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/NotificationUtils.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/NotificationUtils.kt
@@ -314,13 +314,13 @@ class NotificationUtils @Inject constructor(
addAction(replyAction)
}
- @Suppress("MayBeConstant")
companion object : NotificationUtilsCore() {
private val TAG: String = NotificationUtils::class.java.simpleName
private const val DELAY_DEBOUNCE_NOTIF_MS = 1_500L
+ @Suppress("MayBeConstant")
private val defaultSmallIcon = R.drawable.ic_logo_notification
const val DRAFT_ACTIONS_ID = 1
diff --git a/app/src/main/java/com/infomaniak/mail/utils/UiUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/UiUtils.kt
index 3873d5a158..f21c744572 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/UiUtils.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/UiUtils.kt
@@ -37,6 +37,7 @@ import com.infomaniak.mail.utils.extensions.updateNavigationBarColor
object UiUtils {
const val FULLY_SLID = 1.0f
+ const val IGNORE_DIVIDER_TAG = "ignoreDividerTag"
const val PRIMARY_COLOR_CODE = "--kmail-primary-color"
@ColorInt
diff --git a/app/src/main/java/com/infomaniak/mail/utils/Utils.kt b/app/src/main/java/com/infomaniak/mail/utils/Utils.kt
index f3167aa2ba..67ee2300b4 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/Utils.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/Utils.kt
@@ -78,17 +78,27 @@ object Utils {
}
fun waitInitMediator(liveData1: LiveData, liveData2: LiveData): MediatorLiveData> {
+ return waitInitMediator(
+ liveData1,
+ liveData2,
+ constructor = {
+ @Suppress("UNCHECKED_CAST")
+ it[0] as T1 to it[1] as T2
+ },
+ )
+ }
- fun areLiveDataInitialized() = liveData1.isInitialized && liveData2.isInitialized
-
- fun MediatorLiveData>.postIfInit() {
- @Suppress("UNCHECKED_CAST")
- if (areLiveDataInitialized()) postValue((liveData1.value as T1) to (liveData2.value as T2))
- }
-
- return MediatorLiveData>().apply {
- addSource(liveData1) { postIfInit() }
- addSource(liveData2) { postIfInit() }
+ fun waitInitMediator(vararg liveData: LiveData<*>, constructor: (List) -> T): MediatorLiveData {
+ return MediatorLiveData().apply {
+ liveData.forEach { singleLiveData ->
+ addSource(singleLiveData) {
+ if (liveData.all { it.isInitialized }) {
+ val values = liveData.map { it.value }
+ @Suppress("UNCHECKED_CAST")
+ postValue(constructor(values as List))
+ }
+ }
+ }
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt
index 2536232c4c..e951ba099c 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt
@@ -80,6 +80,7 @@ import com.infomaniak.mail.BuildConfig
import com.infomaniak.mail.MainApplication
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings.ThreadDensity
+import com.infomaniak.mail.data.cache.mailboxContent.FolderController
import com.infomaniak.mail.data.models.Attachment
import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.Folder.FolderRole
@@ -103,11 +104,11 @@ import com.infomaniak.mail.ui.main.thread.ThreadFragment.HeaderState
import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiRecipients
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.ApiErrorException
+import com.infomaniak.mail.utils.JsoupParserUtil.jsoupParseWithLog
import com.infomaniak.mail.utils.UiUtils
import com.infomaniak.mail.utils.Utils
import com.infomaniak.mail.utils.Utils.TAG_SEPARATOR
import com.infomaniak.mail.utils.Utils.isPermanentDeleteFolder
-import com.infomaniak.mail.utils.JsoupParserUtil.jsoupParseWithLog
import com.infomaniak.mail.utils.Utils.kSyncAccountUri
import com.infomaniak.mail.utils.WebViewUtils
import io.realm.kotlin.MutableRealm
@@ -116,11 +117,11 @@ import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.copyFromRealm
import io.realm.kotlin.ext.isManaged
import io.realm.kotlin.ext.query
+import io.realm.kotlin.query.RealmQuery
import io.realm.kotlin.query.Sort
import io.realm.kotlin.types.RealmInstant
import io.realm.kotlin.types.RealmObject
import kotlinx.serialization.encodeToString
-import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.util.Calendar
import java.util.Date
@@ -322,29 +323,6 @@ fun LiveData.valueOrEmpty(): List = value?.recipients ?
//endregion
//region Folders
-fun List.getMenuFolders(): Pair, List> {
- return toMutableList().let { list ->
-
- val defaultFolders = list
- .filter { it.role != null }
- .sortedBy { it.role?.order }
- .flattenFolderChildren()
- .also(list::removeAll)
-
- val customFolders = list.flattenFolderChildren()
-
- defaultFolders to customFolders
- }
-}
-
-fun List.getDefaultMenuFolders(): List {
- return sortedBy { it.role?.order }.flattenFolderChildren()
-}
-
-fun List.getCustomMenuFolders(dismissHiddenChildren: Boolean = false): List {
- return flattenFolderChildren(dismissHiddenChildren)
-}
-
fun List.flattenFolderChildren(dismissHiddenChildren: Boolean = false): List {
if (isEmpty()) return this
@@ -356,25 +334,54 @@ fun List.flattenFolderChildren(dismissHiddenChildren: Boolean = false):
val folder = inputList.removeFirst()
- if (folder.isManaged()) {
+ val children = if (folder.isManaged()) {
outputList.add(folder.copyFromRealm(1u))
- val children = with(folder.children) {
+ with(folder.children) {
(if (dismissHiddenChildren) query("${Folder::isHidden.name} == false") else query())
- .sort(Folder::name.name, Sort.ASCENDING)
+ .sortFolders()
.find()
}
- inputList.addAll(0, children)
} else {
outputList.add(folder)
- val children = with(folder.children) { if (dismissHiddenChildren) filter { !it.isHidden } else this }
- inputList.addAll(children)
+ (if (dismissHiddenChildren) folder.children.filter { !it.isHidden } else folder.children)
+ .sortFolders()
}
+ inputList.addAll(index = 0, children)
+
return if (inputList.isEmpty()) outputList else formatFolderWithAllChildren(inputList, outputList)
}
return formatFolderWithAllChildren(toMutableList())
}
+
+/**
+ * These 2 `sortFolders()` functions should always implement the same sort logic.
+ */
+fun RealmQuery.sortFolders() = sort(Folder::sortedName.name, Sort.ASCENDING)
+ .sort(Folder::isFavorite.name, Sort.DESCENDING)
+ .sort(Folder::roleOrder.name, Sort.DESCENDING)
+
+/**
+ * These 2 `sortFolders()` functions should always implement the same sort logic.
+ */
+fun List.sortFolders() = sortedBy { it.sortedName }
+ .sortedByDescending { it.isFavorite }
+ .sortedByDescending { it.roleOrder }
+
+fun List.addDividerBeforeFirstCustomFolder(dividerType: Any): List {
+ val folders = this
+ var needsToAddDivider = true
+ return buildList {
+ folders.forEach { folder ->
+ if (needsToAddDivider && folder.isRootAndCustom) {
+ needsToAddDivider = false
+ add(dividerType)
+ }
+ add(folder)
+ }
+ }
+}
//endregion
//region Messages
@@ -631,6 +638,20 @@ fun Fragment.bindAlertToViewLifecycle(alertDialog: BaseAlertDialog) {
alertDialog.bindAlertToLifecycle(viewLifecycleOwner)
}
+/**
+ * Asynchronously validate folder name locally
+ * @return error string, otherwise null
+ */
+private val invalidCharactersRegex by lazy { Regex("[/'\"]") }
+fun Context.getFolderCreationError(folderName: CharSequence, folderController: FolderController): String? {
+ return when {
+ folderName.length > 255 -> getString(R.string.errorNewFolderNameTooLong)
+ folderName.contains(invalidCharactersRegex) -> getString(R.string.errorNewFolderInvalidCharacter)
+ folderController.getRootFolder(folderName.toString()) != null -> getString(R.string.errorNewFolderAlreadyExists)
+ else -> null
+ }
+}
+
fun Context.getTransparentColor() = getColor(android.R.color.transparent)
fun TypedArray.getColorOrNull(index: Int): Int? = runCatching { getColorOrThrow(index) }.getOrNull()
diff --git a/app/src/main/java/com/infomaniak/mail/views/BottomSheetScaffoldingView.kt b/app/src/main/java/com/infomaniak/mail/views/BottomSheetScaffoldingView.kt
index 46c8ce36a4..7f42518741 100644
--- a/app/src/main/java/com/infomaniak/mail/views/BottomSheetScaffoldingView.kt
+++ b/app/src/main/java/com/infomaniak/mail/views/BottomSheetScaffoldingView.kt
@@ -31,7 +31,7 @@ import com.infomaniak.lib.core.utils.getAttributes
import com.infomaniak.lib.core.utils.setMarginsRelative
import com.infomaniak.mail.R
import com.infomaniak.mail.databinding.ViewBottomSheetScaffoldingBinding
-import com.infomaniak.mail.ui.main.menu.SimpleSettingView
+import com.infomaniak.mail.ui.main.menuDrawer.SimpleSettingView
import com.infomaniak.lib.core.R as RCore
class BottomSheetScaffoldingView @JvmOverloads constructor(
diff --git a/app/src/main/java/com/infomaniak/mail/views/itemViews/SelectableItemView.kt b/app/src/main/java/com/infomaniak/mail/views/itemViews/SelectableItemView.kt
index ca9bae78b7..0ab01e3bdc 100644
--- a/app/src/main/java/com/infomaniak/mail/views/itemViews/SelectableItemView.kt
+++ b/app/src/main/java/com/infomaniak/mail/views/itemViews/SelectableItemView.kt
@@ -20,8 +20,10 @@ package com.infomaniak.mail.views.itemViews
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
+import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import com.infomaniak.mail.R
+import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.utils.extensions.getAttributeColor
import com.google.android.material.R as RMaterial
@@ -50,3 +52,9 @@ sealed class SelectableItemView @JvmOverloads constructor(
setEndIcon(if (isSelected) checkIcon else null, R.string.contentDescriptionSelectedItem)
}
}
+
+fun SelectableItemView.setFolderUi(folder: Folder, @DrawableRes iconId: Int, isSelected: Boolean) {
+ text = folder.getLocalizedName(context)
+ icon = AppCompatResources.getDrawable(context, iconId)
+ setSelectedState(isSelected)
+}
diff --git a/app/src/main/java/com/infomaniak/mail/views/itemViews/UnreadFolderItemView.kt b/app/src/main/java/com/infomaniak/mail/views/itemViews/UnreadFolderItemView.kt
index dc59756818..6e1c91c95e 100644
--- a/app/src/main/java/com/infomaniak/mail/views/itemViews/UnreadFolderItemView.kt
+++ b/app/src/main/java/com/infomaniak/mail/views/itemViews/UnreadFolderItemView.kt
@@ -32,12 +32,6 @@ class UnreadFolderItemView @JvmOverloads constructor(
private var onCollapsedFolderClicked: OnClickListener? = null
- var isHidden = false
- set(value) {
- field = value
- binding.root.isVisible = !value
- }
-
override var isCollapsed = false
set(value) {
field = value
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 25e06ea757..fdb641aef3 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -46,7 +46,7 @@
diff --git a/app/src/main/res/layout/bottom_sheet_restore_emails.xml b/app/src/main/res/layout/bottom_sheet_restore_emails.xml
index 70bdd8fb90..1bfe6fea15 100644
--- a/app/src/main/res/layout/bottom_sheet_restore_emails.xml
+++ b/app/src/main/res/layout/bottom_sheet_restore_emails.xml
@@ -19,7 +19,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".ui.main.menu.RestoreEmailsBottomSheetDialog">
+ tools:context=".ui.main.menuDrawer.RestoreEmailsBottomSheetDialog">
.
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml
index 7d32d1d6a1..021c64cb7c 100644
--- a/app/src/main/res/layout/fragment_account.xml
+++ b/app/src/main/res/layout/fragment_account.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_ai_engine_setting.xml b/app/src/main/res/layout/fragment_ai_engine_setting.xml
index a98e4e79c1..3f7e25e709 100644
--- a/app/src/main/res/layout/fragment_ai_engine_setting.xml
+++ b/app/src/main/res/layout/fragment_ai_engine_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_attach_mailbox.xml b/app/src/main/res/layout/fragment_attach_mailbox.xml
index 8145546a67..2a3db7dbee 100644
--- a/app/src/main/res/layout/fragment_attach_mailbox.xml
+++ b/app/src/main/res/layout/fragment_attach_mailbox.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_auto_advance_settings.xml b/app/src/main/res/layout/fragment_auto_advance_settings.xml
index bd2a9f6757..e6e5b71766 100644
--- a/app/src/main/res/layout/fragment_auto_advance_settings.xml
+++ b/app/src/main/res/layout/fragment_auto_advance_settings.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_cancel_delay_setting.xml b/app/src/main/res/layout/fragment_cancel_delay_setting.xml
index 4b5530032c..014cda1d37 100644
--- a/app/src/main/res/layout/fragment_cancel_delay_setting.xml
+++ b/app/src/main/res/layout/fragment_cancel_delay_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_data_management_matomo_setting.xml b/app/src/main/res/layout/fragment_data_management_matomo_setting.xml
index 6db8f35a4e..2c4a925e9e 100644
--- a/app/src/main/res/layout/fragment_data_management_matomo_setting.xml
+++ b/app/src/main/res/layout/fragment_data_management_matomo_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_data_management_sentry_setting.xml b/app/src/main/res/layout/fragment_data_management_sentry_setting.xml
index ec062589a4..425f071a0a 100644
--- a/app/src/main/res/layout/fragment_data_management_sentry_setting.xml
+++ b/app/src/main/res/layout/fragment_data_management_sentry_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_data_management_settings.xml b/app/src/main/res/layout/fragment_data_management_settings.xml
index 2f7b36e44d..bc1d7194ba 100644
--- a/app/src/main/res/layout/fragment_data_management_settings.xml
+++ b/app/src/main/res/layout/fragment_data_management_settings.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_external_content_setting.xml b/app/src/main/res/layout/fragment_external_content_setting.xml
index 0cbef0438d..e562404f0b 100644
--- a/app/src/main/res/layout/fragment_external_content_setting.xml
+++ b/app/src/main/res/layout/fragment_external_content_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_forward_mails_setting.xml b/app/src/main/res/layout/fragment_forward_mails_setting.xml
index acac90da3f..d95723dc81 100644
--- a/app/src/main/res/layout/fragment_forward_mails_setting.xml
+++ b/app/src/main/res/layout/fragment_forward_mails_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_invalid_password.xml b/app/src/main/res/layout/fragment_invalid_password.xml
index 36729497a2..6ff1e9888b 100644
--- a/app/src/main/res/layout/fragment_invalid_password.xml
+++ b/app/src/main/res/layout/fragment_invalid_password.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_mailbox_settings.xml b/app/src/main/res/layout/fragment_mailbox_settings.xml
index 8ac4faf7bd..fc781c3f40 100644
--- a/app/src/main/res/layout/fragment_mailbox_settings.xml
+++ b/app/src/main/res/layout/fragment_mailbox_settings.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_menu_drawer.xml b/app/src/main/res/layout/fragment_menu_drawer.xml
index a6e462be32..ab69cabfbe 100644
--- a/app/src/main/res/layout/fragment_menu_drawer.xml
+++ b/app/src/main/res/layout/fragment_menu_drawer.xml
@@ -60,268 +60,13 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginVertical="@dimen/marginStandardSmall"
+ android:scrollbars="none"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ tools:itemCount="3"
+ tools:listitem="@layout/item_menu_drawer_mailbox" />
diff --git a/app/src/main/res/layout/fragment_move.xml b/app/src/main/res/layout/fragment_move.xml
index e6f680dd0f..78bc087b46 100644
--- a/app/src/main/res/layout/fragment_move.xml
+++ b/app/src/main/res/layout/fragment_move.xml
@@ -91,62 +91,18 @@
android:hint="@string/moveSearchFieldPlaceholder" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ android:background="@color/backgroundColor"
+ android:clipToPadding="false"
+ android:fillViewport="true"
+ android:paddingBottom="@dimen/marginStandardMedium"
+ android:scrollbars="none"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ tools:itemCount="6"
+ tools:listitem="@layout/item_menu_drawer_folder" />
diff --git a/app/src/main/res/layout/fragment_send_settings.xml b/app/src/main/res/layout/fragment_send_settings.xml
index 8988e91455..78277cc425 100644
--- a/app/src/main/res/layout/fragment_send_settings.xml
+++ b/app/src/main/res/layout/fragment_send_settings.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index b5ce44e272..b0b517f673 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_signature_setting.xml b/app/src/main/res/layout/fragment_signature_setting.xml
index 4c46390fc7..ebcd1d3854 100644
--- a/app/src/main/res/layout/fragment_signature_setting.xml
+++ b/app/src/main/res/layout/fragment_signature_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_swipe_actions_selection_setting.xml b/app/src/main/res/layout/fragment_swipe_actions_selection_setting.xml
index 45a2ac87b4..710fc4117b 100644
--- a/app/src/main/res/layout/fragment_swipe_actions_selection_setting.xml
+++ b/app/src/main/res/layout/fragment_swipe_actions_selection_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_swipe_actions_settings.xml b/app/src/main/res/layout/fragment_swipe_actions_settings.xml
index 158e9b9ba6..dfeb8875f2 100644
--- a/app/src/main/res/layout/fragment_swipe_actions_settings.xml
+++ b/app/src/main/res/layout/fragment_swipe_actions_settings.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_switch_user.xml b/app/src/main/res/layout/fragment_switch_user.xml
index 5fffc5a9fe..37a0c50520 100644
--- a/app/src/main/res/layout/fragment_switch_user.xml
+++ b/app/src/main/res/layout/fragment_switch_user.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_theme_setting.xml b/app/src/main/res/layout/fragment_theme_setting.xml
index 862b566e78..c866e2b96a 100644
--- a/app/src/main/res/layout/fragment_theme_setting.xml
+++ b/app/src/main/res/layout/fragment_theme_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_thread_list_density_setting.xml b/app/src/main/res/layout/fragment_thread_list_density_setting.xml
index c304c330e6..9309c01476 100644
--- a/app/src/main/res/layout/fragment_thread_list_density_setting.xml
+++ b/app/src/main/res/layout/fragment_thread_list_density_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/fragment_thread_mode_setting.xml b/app/src/main/res/layout/fragment_thread_mode_setting.xml
index e44cb20678..258a544355 100644
--- a/app/src/main/res/layout/fragment_thread_mode_setting.xml
+++ b/app/src/main/res/layout/fragment_thread_mode_setting.xml
@@ -15,7 +15,7 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see .
-->
-
-
+
diff --git a/app/src/main/res/layout/item_divider_horizontal.xml b/app/src/main/res/layout/item_divider_horizontal.xml
index 0314249af6..f3e3c6c0f7 100644
--- a/app/src/main/res/layout/item_divider_horizontal.xml
+++ b/app/src/main/res/layout/item_divider_horizontal.xml
@@ -19,4 +19,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="1dp"
+ android:layout_marginStart="@dimen/dividerHorizontalPadding"
+ android:layout_marginEnd="@dimen/dividerHorizontalPadding"
app:dividerColor="@color/dividerColor" />
diff --git a/app/src/main/res/layout/item_menu_drawer_action.xml b/app/src/main/res/layout/item_menu_drawer_action.xml
new file mode 100644
index 0000000000..71f86fe75b
--- /dev/null
+++ b/app/src/main/res/layout/item_menu_drawer_action.xml
@@ -0,0 +1,24 @@
+
+
diff --git a/app/src/main/res/layout/item_menu_drawer_actions_header.xml b/app/src/main/res/layout/item_menu_drawer_actions_header.xml
new file mode 100644
index 0000000000..77b1a5e435
--- /dev/null
+++ b/app/src/main/res/layout/item_menu_drawer_actions_header.xml
@@ -0,0 +1,24 @@
+
+
diff --git a/app/src/main/res/layout/item_menu_drawer_custom_folders_header.xml b/app/src/main/res/layout/item_menu_drawer_custom_folders_header.xml
new file mode 100644
index 0000000000..96ca2f4481
--- /dev/null
+++ b/app/src/main/res/layout/item_menu_drawer_custom_folders_header.xml
@@ -0,0 +1,25 @@
+
+
diff --git a/app/src/main/res/layout/item_menu_drawer_divider.xml b/app/src/main/res/layout/item_menu_drawer_divider.xml
new file mode 100644
index 0000000000..00bc4ccfdb
--- /dev/null
+++ b/app/src/main/res/layout/item_menu_drawer_divider.xml
@@ -0,0 +1,21 @@
+
+
diff --git a/app/src/main/res/layout/item_menu_drawer_empty_custom_folders.xml b/app/src/main/res/layout/item_menu_drawer_empty_custom_folders.xml
new file mode 100644
index 0000000000..6970acfce2
--- /dev/null
+++ b/app/src/main/res/layout/item_menu_drawer_empty_custom_folders.xml
@@ -0,0 +1,28 @@
+
+
diff --git a/app/src/main/res/layout/item_folder_menu_drawer.xml b/app/src/main/res/layout/item_menu_drawer_folder.xml
similarity index 100%
rename from app/src/main/res/layout/item_folder_menu_drawer.xml
rename to app/src/main/res/layout/item_menu_drawer_folder.xml
diff --git a/app/src/main/res/layout/item_menu_drawer_footer.xml b/app/src/main/res/layout/item_menu_drawer_footer.xml
new file mode 100644
index 0000000000..b47b877206
--- /dev/null
+++ b/app/src/main/res/layout/item_menu_drawer_footer.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_mailbox_menu_drawer.xml b/app/src/main/res/layout/item_menu_drawer_mailbox.xml
similarity index 100%
rename from app/src/main/res/layout/item_mailbox_menu_drawer.xml
rename to app/src/main/res/layout/item_menu_drawer_mailbox.xml
diff --git a/app/src/main/res/layout/item_menu_drawer_mailboxes_header.xml b/app/src/main/res/layout/item_menu_drawer_mailboxes_header.xml
new file mode 100644
index 0000000000..a8a349c4f6
--- /dev/null
+++ b/app/src/main/res/layout/item_menu_drawer_mailboxes_header.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml
index e0e4b5c552..fc1a280c64 100644
--- a/app/src/main/res/navigation/main_navigation.xml
+++ b/app/src/main/res/navigation/main_navigation.xml
@@ -254,7 +254,7 @@
diff --git a/fastlane/metadata/android/fr/changelogs/1_04_002_01.txt b/fastlane/metadata/android/fr/changelogs/1_04_002_01.txt
index f6e78c29a1..ecae35e131 100644
--- a/fastlane/metadata/android/fr/changelogs/1_04_002_01.txt
+++ b/fastlane/metadata/android/fr/changelogs/1_04_002_01.txt
@@ -1,2 +1,2 @@
-- Bloquage de l'importation de pièces jointes lorsque la limite de taille est atteinte
+- Blocage de l'importation de pièces jointes lorsque la limite de taille est atteinte
- Correction de la barre d'outils de l'éditeur affichée sous le clavier