Skip to content

Commit e4c390a

Browse files
Merge pull request #1989 from Infomaniak/nested-scroll-views
Remove NestedScrollViews in Move & MenuDrawer fragments
2 parents d9c1761 + acd1034 commit e4c390a

File tree

104 files changed

+2299
-1433
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+2299
-1433
lines changed

.idea/codeStyles/Project.xml

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src/main/java/com/infomaniak/mail/MatomoMail.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ object MatomoMail : MatomoCore {
168168
}
169169

170170
fun Fragment.trackCreateFolderEvent(name: String) {
171+
context?.trackCreateFolderEvent(name)
172+
}
173+
174+
fun Context.trackCreateFolderEvent(name: String) {
171175
trackEvent("createFolder", name)
172176
}
173177

@@ -209,10 +213,6 @@ object MatomoMail : MatomoCore {
209213
trackEvent("invalidPasswordMailbox", name)
210214
}
211215

212-
fun Fragment.trackExternalEvent(name: String, action: TrackerAction = TrackerAction.CLICK, value: Float? = null) {
213-
context?.trackExternalEvent(name, action, value)
214-
}
215-
216216
fun Context.trackExternalEvent(name: String, action: TrackerAction = TrackerAction.CLICK, value: Float? = null) {
217217
trackEvent("externals", name, action, value)
218218
}

app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ package com.infomaniak.mail.data.api
2020
import com.infomaniak.lib.core.InfomaniakCore
2121
import com.infomaniak.lib.core.R
2222
import com.infomaniak.lib.core.api.ApiController
23-
import com.infomaniak.lib.core.api.ApiController.ApiMethod.*
23+
import com.infomaniak.lib.core.api.ApiController.ApiMethod.DELETE
24+
import com.infomaniak.lib.core.api.ApiController.ApiMethod.GET
25+
import com.infomaniak.lib.core.api.ApiController.ApiMethod.PATCH
26+
import com.infomaniak.lib.core.api.ApiController.ApiMethod.POST
27+
import com.infomaniak.lib.core.api.ApiController.ApiMethod.PUT
2428
import com.infomaniak.lib.core.api.ApiRepositoryCore
2529
import com.infomaniak.lib.core.models.ApiResponse
2630
import com.infomaniak.lib.core.models.ApiResponseStatus

app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt

+28-28
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.infomaniak.mail.data.models.Folder.FolderRole
2525
import com.infomaniak.mail.data.models.mailbox.Mailbox
2626
import com.infomaniak.mail.utils.extensions.copyListToRealm
2727
import com.infomaniak.mail.utils.extensions.flattenFolderChildren
28+
import com.infomaniak.mail.utils.extensions.sortFolders
2829
import io.realm.kotlin.MutableRealm
2930
import io.realm.kotlin.Realm
3031
import io.realm.kotlin.TypedRealm
@@ -34,7 +35,6 @@ import io.realm.kotlin.notifications.ResultsChange
3435
import io.realm.kotlin.query.RealmQuery
3536
import io.realm.kotlin.query.RealmResults
3637
import io.realm.kotlin.query.RealmSingleQuery
37-
import io.realm.kotlin.query.Sort
3838
import kotlinx.coroutines.flow.Flow
3939
import kotlinx.coroutines.flow.mapNotNull
4040
import javax.inject.Inject
@@ -45,20 +45,20 @@ class FolderController @Inject constructor(
4545
) {
4646

4747
//region Get data
48-
fun getCustomFolders(): RealmResults<Folder> {
49-
return getCustomFoldersQuery(mailboxContentRealm()).find()
48+
fun getMenuDrawerDefaultFoldersAsync(): Flow<ResultsChange<Folder>> {
49+
return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.CUSTOM, withoutChildren = true).asFlow()
5050
}
5151

52-
fun getRootsFoldersAsync(): Flow<ResultsChange<Folder>> {
53-
return getFoldersQuery(mailboxContentRealm(), onlyRoots = true).asFlow()
52+
fun getMenuDrawerCustomFoldersAsync(): Flow<ResultsChange<Folder>> {
53+
return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.DEFAULT, withoutChildren = true).asFlow()
5454
}
5555

56-
fun getDefaultFoldersAsync(): Flow<ResultsChange<Folder>> {
57-
return getDefaultFoldersQuery(mailboxContentRealm()).asFlow()
56+
fun getSearchFoldersAsync(): Flow<ResultsChange<Folder>> {
57+
return getFoldersQuery(mailboxContentRealm(), withoutChildren = true).asFlow()
5858
}
5959

60-
fun getCustomFoldersAsync(): Flow<ResultsChange<Folder>> {
61-
return getCustomFoldersQuery(mailboxContentRealm()).asFlow()
60+
fun getMoveFolders(): RealmResults<Folder> {
61+
return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.DRAFT, withoutChildren = true).find()
6262
}
6363

6464
fun getFolder(id: String): Folder? {
@@ -131,31 +131,31 @@ class FolderController @Inject constructor(
131131
}
132132
//endregion
133133

134+
enum class FoldersType {
135+
DEFAULT,
136+
CUSTOM,
137+
DRAFT,
138+
}
139+
134140
companion object {
135141
const val SEARCH_FOLDER_ID = "search_folder_id"
136142
private val isNotSearch = "${Folder::id.name} != '$SEARCH_FOLDER_ID'"
137143
private val isRootFolder = "${Folder.parentsPropertyName}.@count == 0"
138144

139145
//region Queries
140-
private fun getFoldersQuery(realm: TypedRealm, onlyRoots: Boolean = false): RealmQuery<Folder> {
141-
val rootsQuery = if (onlyRoots) " AND $isRootFolder" else ""
142-
return realm
143-
.query<Folder>(isNotSearch + rootsQuery)
144-
.sort(Folder::name.name, Sort.ASCENDING)
145-
.sort(Folder::isFavorite.name, Sort.DESCENDING)
146-
}
147-
148-
private fun getDefaultFoldersQuery(realm: TypedRealm): RealmQuery<Folder> {
149-
val hasRole = "${Folder.rolePropertyName} != nil"
150-
return realm.query("$isNotSearch AND $hasRole")
151-
}
152-
153-
private fun getCustomFoldersQuery(realm: TypedRealm): RealmQuery<Folder> {
154-
val hasNoRole = "${Folder.rolePropertyName} == nil"
155-
return realm
156-
.query<Folder>("$isNotSearch AND $isRootFolder AND $hasNoRole")
157-
.sort(Folder::name.name, Sort.ASCENDING)
158-
.sort(Folder::isFavorite.name, Sort.DESCENDING)
146+
private fun getFoldersQuery(
147+
realm: TypedRealm,
148+
withoutType: FoldersType? = null,
149+
withoutChildren: Boolean = false,
150+
): RealmQuery<Folder> {
151+
val rootsQuery = if (withoutChildren) " AND $isRootFolder" else ""
152+
val typeQuery = when (withoutType) {
153+
FoldersType.DEFAULT -> " AND ${Folder.rolePropertyName} == nil"
154+
FoldersType.CUSTOM -> " AND ${Folder.rolePropertyName} != nil"
155+
FoldersType.DRAFT -> " AND ${Folder.rolePropertyName} != '${FolderRole.DRAFT.name}'"
156+
null -> ""
157+
}
158+
return realm.query<Folder>("$isNotSearch${rootsQuery}${typeQuery}").sortFolders()
159159
}
160160

161161
private fun getFoldersQuery(exceptionsFoldersIds: List<String>, realm: TypedRealm): RealmQuery<Folder> {

app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import com.infomaniak.lib.core.utils.SentryLog
2222
import com.infomaniak.mail.data.LocalSettings
2323
import com.infomaniak.mail.data.LocalSettings.ThreadMode
2424
import com.infomaniak.mail.data.api.ApiRepository
25-
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode.*
25+
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode.ONE_PAGE_OF_OLD_MESSAGES
26+
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode.REFRESH_FOLDER
27+
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode.REFRESH_FOLDER_WITH_ROLE
2628
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RetryStrategy.Iteration
2729
import com.infomaniak.mail.data.cache.mailboxInfo.MailboxController
2830
import com.infomaniak.mail.data.models.Folder

app/src/main/java/com/infomaniak/mail/data/models/Folder.kt

+19-8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.content.Context
2323
import androidx.annotation.DrawableRes
2424
import androidx.annotation.StringRes
2525
import com.infomaniak.lib.core.utils.Utils.enumValueOfOrNull
26+
import com.infomaniak.lib.core.utils.removeAccents
2627
import com.infomaniak.mail.R
2728
import com.infomaniak.mail.data.models.message.Message
2829
import com.infomaniak.mail.data.models.thread.Thread
@@ -43,7 +44,7 @@ import kotlinx.serialization.UseSerializers
4344
import kotlin.math.max
4445

4546
@Serializable
46-
class Folder : RealmObject {
47+
class Folder : RealmObject, Cloneable {
4748

4849
//region Remote data
4950
@PrimaryKey
@@ -79,6 +80,10 @@ class Folder : RealmObject {
7980
var isHidden: Boolean = false // For children only (a children Folder is hidden if its parent is collapsed)
8081
@Transient
8182
var isCollapsed: Boolean = false // For parents only (collapsing a parent Folder will hide its children)
83+
@Transient
84+
var roleOrder: Int = role?.order ?: CUSTOM_FOLDER_ROLE_ORDER
85+
@Transient
86+
var sortedName: String = name
8287
//endregion
8388

8489
private val _parents by backlinks(Folder::children)
@@ -99,6 +104,9 @@ class Folder : RealmObject {
99104
val isRoot: Boolean
100105
inline get() = !path.contains(separator)
101106

107+
val isRootAndCustom: Boolean
108+
inline get() = role == null && isRoot
109+
102110
fun initLocalValues(
103111
lastUpdatedAt: RealmInstant?,
104112
cursor: String?,
@@ -120,6 +128,8 @@ class Folder : RealmObject {
120128
this.isHistoryComplete = isHistoryComplete
121129
this.isHidden = isHidden
122130
this.isCollapsed = isCollapsed
131+
132+
this.sortedName = this.name.lowercase().removeAccents()
123133
}
124134

125135
fun resetLocalValues() {
@@ -153,14 +163,14 @@ class Folder : RealmObject {
153163
val order: Int,
154164
val matomoValue: String,
155165
) {
156-
INBOX(R.string.inboxFolder, R.drawable.ic_drawer_inbox, 0, "inboxFolder"),
166+
INBOX(R.string.inboxFolder, R.drawable.ic_drawer_inbox, 8, "inboxFolder"),
167+
COMMERCIAL(R.string.commercialFolder, R.drawable.ic_promotions, 7, "commercialFolder"),
168+
SOCIALNETWORKS(R.string.socialNetworksFolder, R.drawable.ic_social_media, 6, "socialNetworksFolder"),
169+
SENT(R.string.sentFolder, R.drawable.ic_sent_messages, 5, "sentFolder"),
157170
DRAFT(R.string.draftFolder, R.drawable.ic_draft, 4, "draftFolder"),
158-
SENT(R.string.sentFolder, R.drawable.ic_sent_messages, 3, "sentFolder"),
159-
SPAM(R.string.spamFolder, R.drawable.ic_spam, 5, "spamFolder"),
160-
TRASH(R.string.trashFolder, R.drawable.ic_bin, 6, "trashFolder"),
161-
ARCHIVE(R.string.archiveFolder, R.drawable.ic_archive_folder, 7, "archiveFolder"),
162-
COMMERCIAL(R.string.commercialFolder, R.drawable.ic_promotions, 1, "commercialFolder"),
163-
SOCIALNETWORKS(R.string.socialNetworksFolder, R.drawable.ic_social_media, 2, "socialNetworksFolder"),
171+
SPAM(R.string.spamFolder, R.drawable.ic_spam, 3, "spamFolder"),
172+
TRASH(R.string.trashFolder, R.drawable.ic_bin, 2, "trashFolder"),
173+
ARCHIVE(R.string.archiveFolder, R.drawable.ic_archive_folder, 1, "archiveFolder"),
164174
}
165175

166176
companion object {
@@ -173,5 +183,6 @@ class Folder : RealmObject {
173183
const val DEFAULT_IS_HISTORY_COMPLETE = false
174184

175185
const val INBOX_FOLDER_ID = "eJzz9HPyjwAABGYBgQ--"
186+
private const val CUSTOM_FOLDER_ROLE_ORDER = 0
176187
}
177188
}

app/src/main/java/com/infomaniak/mail/data/models/Quotas.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class Quotas : EmbeddedRealmObject {
3131
@SerialName("size")
3232
private var _size: Long = 0L
3333

34-
private val size: Long get() = _size * 1_000L // Convert from KiloOctets to Octets
34+
val size: Long get() = _size * 1_000L // Convert from KiloOctets to Octets
3535

3636
fun getText(context: Context): String {
3737

app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt

+1-5
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,9 @@ import com.infomaniak.mail.data.models.draft.Draft.DraftAction
6262
import com.infomaniak.mail.databinding.ActivityMainBinding
6363
import com.infomaniak.mail.firebase.RegisterFirebaseBroadcastReceiver
6464
import com.infomaniak.mail.ui.alertDialogs.DescriptionAlertDialog
65-
import com.infomaniak.mail.ui.alertDialogs.TitleAlertDialog
6665
import com.infomaniak.mail.ui.main.SnackbarManager
6766
import com.infomaniak.mail.ui.main.folder.TwoPaneFragment
68-
import com.infomaniak.mail.ui.main.menu.MenuDrawerFragment
67+
import com.infomaniak.mail.ui.main.menuDrawer.MenuDrawerFragment
6968
import com.infomaniak.mail.ui.main.onboarding.PermissionsOnboardingPagerFragment
7069
import com.infomaniak.mail.ui.main.search.SearchFragmentArgs
7170
import com.infomaniak.mail.ui.newMessage.NewMessageActivity
@@ -144,9 +143,6 @@ class MainActivity : BaseActivity() {
144143
@Inject
145144
lateinit var descriptionDialog: DescriptionAlertDialog
146145

147-
@Inject
148-
lateinit var titleDialog: TitleAlertDialog
149-
150146
@Inject
151147
lateinit var permissionUtils: PermissionUtils
152148

app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt

+6-9
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ import com.infomaniak.mail.utils.Utils.runCatchingRealm
6969
import com.infomaniak.mail.utils.coroutineContext
7070
import com.infomaniak.mail.utils.extensions.MergedContactDictionary
7171
import com.infomaniak.mail.utils.extensions.appContext
72-
import com.infomaniak.mail.utils.extensions.getCustomMenuFolders
73-
import com.infomaniak.mail.utils.extensions.getDefaultMenuFolders
72+
import com.infomaniak.mail.utils.extensions.flattenFolderChildren
7473
import com.infomaniak.mail.utils.extensions.getFoldersIds
7574
import com.infomaniak.mail.utils.extensions.getUids
7675
import com.infomaniak.mail.utils.extensions.launchNoValidMailboxesActivity
@@ -85,6 +84,7 @@ import kotlinx.coroutines.Job
8584
import kotlinx.coroutines.delay
8685
import kotlinx.coroutines.flow.MutableStateFlow
8786
import kotlinx.coroutines.flow.emptyFlow
87+
import kotlinx.coroutines.flow.filterNotNull
8888
import kotlinx.coroutines.flow.flatMapLatest
8989
import kotlinx.coroutines.flow.map
9090
import kotlinx.coroutines.flow.mapLatest
@@ -119,7 +119,6 @@ class MainViewModel @Inject constructor(
119119
private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher)
120120
private var refreshEverythingJob: Job? = null
121121

122-
// First boolean is the download status, second boolean is if the LoadMore button should be displayed
123122
val isDownloadingChanges: MutableLiveData<Boolean> = MutableLiveData(false)
124123
val isInternetAvailable = MutableLiveData<Boolean>()
125124
val isMovedToNewFolder = SingleLiveEvent<Boolean>()
@@ -158,14 +157,12 @@ class MainViewModel @Inject constructor(
158157
it?.let(mailboxController::getMailbox)
159158
}.asLiveData(ioCoroutineContext)
160159

161-
val currentDefaultFoldersLive = _currentMailboxObjectId.flatMapLatest { objectId ->
162-
objectId?.let { folderController.getDefaultFoldersAsync().map { it.list.getDefaultMenuFolders() } } ?: emptyFlow()
160+
val defaultFoldersLive = _currentMailboxObjectId.filterNotNull().flatMapLatest {
161+
folderController.getMenuDrawerDefaultFoldersAsync().map { it.list.flattenFolderChildren(dismissHiddenChildren = true) }
163162
}.asLiveData(ioCoroutineContext)
164163

165-
val currentCustomFoldersLive = _currentMailboxObjectId.flatMapLatest { objectId ->
166-
objectId
167-
?.let { folderController.getCustomFoldersAsync().map { it.list.getCustomMenuFolders(dismissHiddenChildren = true) } }
168-
?: emptyFlow()
164+
val customFoldersLive = _currentMailboxObjectId.filterNotNull().flatMapLatest {
165+
folderController.getMenuDrawerCustomFoldersAsync().map { it.list.flattenFolderChildren(dismissHiddenChildren = true) }
169166
}.asLiveData(ioCoroutineContext)
170167

171168
val currentQuotasLive = _currentMailboxObjectId.flatMapLatest {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Infomaniak Mail - Android
3+
* Copyright (C) 2024 Infomaniak Network SA
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package com.infomaniak.mail.ui.alertDialogs
19+
20+
import android.content.Context
21+
import androidx.annotation.StringRes
22+
import com.infomaniak.mail.MatomoMail.trackCreateFolderEvent
23+
import com.infomaniak.mail.R
24+
import com.infomaniak.mail.data.cache.mailboxContent.FolderController
25+
import com.infomaniak.mail.di.IoDispatcher
26+
import com.infomaniak.mail.utils.extensions.getFolderCreationError
27+
import dagger.hilt.android.qualifiers.ActivityContext
28+
import dagger.hilt.android.scopes.ActivityScoped
29+
import kotlinx.coroutines.CoroutineDispatcher
30+
import javax.inject.Inject
31+
32+
@ActivityScoped
33+
class CreateFolderDialog @Inject constructor(
34+
@ActivityContext private val activityContext: Context,
35+
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
36+
private val folderController: FolderController,
37+
) : InputAlertDialog(activityContext, ioDispatcher) {
38+
39+
fun show(@StringRes confirmButtonText: Int = R.string.buttonCreate) = show(
40+
title = R.string.newFolderDialogTitle,
41+
hint = R.string.newFolderDialogHint,
42+
confirmButtonText = confirmButtonText,
43+
)
44+
45+
fun setCallbacks(onPositiveButtonClicked: (String) -> Unit) = setCallbacks(
46+
onPositiveButtonClicked = { folderName ->
47+
activityContext.trackCreateFolderEvent("confirm")
48+
onPositiveButtonClicked(folderName)
49+
},
50+
onErrorCheck = { folderName ->
51+
activityContext.getFolderCreationError(folderName, folderController)
52+
},
53+
)
54+
}

app/src/main/java/com/infomaniak/mail/ui/alertDialogs/InputAlertDialog.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import javax.inject.Inject
4242
import com.infomaniak.lib.core.R as RCore
4343

4444
@ActivityScoped
45-
class InputAlertDialog @Inject constructor(
45+
open class InputAlertDialog @Inject constructor(
4646
@ActivityContext private val activityContext: Context,
4747
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
4848
) : BaseAlertDialog(activityContext) {

app/src/main/java/com/infomaniak/mail/ui/main/MailboxListFragment.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import com.infomaniak.lib.core.utils.safeNavigate
2323
import com.infomaniak.mail.R
2424
import com.infomaniak.mail.data.models.mailbox.Mailbox
2525
import com.infomaniak.mail.ui.bottomSheetDialogs.LockedMailboxBottomSheetDialogArgs
26-
import com.infomaniak.mail.ui.main.menu.MailboxesAdapter
26+
import com.infomaniak.mail.ui.main.menuDrawer.MailboxesAdapter
2727
import com.infomaniak.mail.utils.AccountUtils
2828
import kotlinx.coroutines.launch
2929

@@ -49,7 +49,7 @@ interface MailboxListFragment {
4949
)
5050
}
5151

52-
fun Fragment.onValidMailboxClicked(mailboxId: Int) {
53-
lifecycleScope.launch { AccountUtils.switchToMailbox(mailboxId) }
52+
fun Fragment.onValidMailboxClicked(mailboxId: Int) = lifecycleScope.launch {
53+
AccountUtils.switchToMailbox(mailboxId)
5454
}
5555
}

0 commit comments

Comments
 (0)