Repository: Infomaniak/android-kMail
Pull request #2353: feat: Display messages coming from the snooze folder as such in the search
Author: login: LunarX
Labels: name: enhancement
description: Improve or expand upon an existing feature
Created: 2025-05-02T07:31:33Z | Merged: 2025-05-07T12:35:10Z

When a search thread has a remote folder id of "inbox" and is snoozed, we need to alter the displayed origin folder name in the search to display "snooze" instead. Snoozed folders are always located inside of inbox, therefore only deriving the displayed origin name from the remote folder id is not enough because of snooze.

Depends on #2347 


Diff:
diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/search/NamedFolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/search/NamedFolder.kt
new file mode 100644
index 00000000000..0a6e9febf62
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/ui/main/search/NamedFolder.kt
@@ -0,0 +1,39 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
+ */
+package com.infomaniak.mail.ui.main.search
+
+import android.content.Context
+import com.infomaniak.mail.data.models.Folder
+
+sealed interface NamedFolder {
+    data class Role(val role: Folder.FolderRole) : NamedFolder {
+        override fun getName(context: Context): String = context.getString(role.folderNameRes)
+    }
+
+    data class Custom(val name: String) : NamedFolder {
+        override fun getName(context: Context): String = name
+    }
+
+    fun getName(context: Context): String
+
+    companion object {
+        fun fromFolder(folder: Folder, context: Context): NamedFolder = folder.role.let { role ->
+            if (role != null) Role(role) else Custom(folder.getLocalizedName(context))
+        }
+    }
+}
diff --git a/app/src/main/java/com/infomaniak/mail/utils/SearchUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/SearchUtils.kt
index 3833bc650bb..618f35367ca 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/SearchUtils.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/SearchUtils.kt
@@ -25,10 +25,12 @@ import com.infomaniak.mail.data.cache.mailboxContent.MessageController
 import com.infomaniak.mail.data.cache.mailboxContent.ThreadController
 import com.infomaniak.mail.data.models.Folder
 import com.infomaniak.mail.data.models.Folder.FolderRole
+import com.infomaniak.mail.data.models.isSnoozed
 import com.infomaniak.mail.data.models.message.Message
 import com.infomaniak.mail.data.models.thread.Thread
 import com.infomaniak.mail.data.models.thread.Thread.ThreadFilter
 import com.infomaniak.mail.di.IoDispatcher
+import com.infomaniak.mail.ui.main.search.NamedFolder
 import dagger.hilt.android.qualifiers.ApplicationContext
 import io.realm.kotlin.Realm
 import io.realm.kotlin.ext.realmListOf
@@ -90,13 +92,13 @@ class SearchUtils @Inject constructor(
     }
 
     fun convertLocalMessagesToSearchThreads(searchMessages: List<Message>): List<Thread> {
-        val cachedFolderNames = mutableMapOf<String, String>()
+        val cachedNamedFolders = mutableMapOf<String, NamedFolder>()
         return searchMessages.map { message ->
             message.toThread().apply {
                 uid = "search-${message.uid}"
                 isFromSearch = true
                 recomputeThread()
-                sharedThreadProcessing(appContext, cachedFolderNames, realm = mailboxContentRealm())
+                sharedThreadProcessing(appContext, cachedNamedFolders, realm = mailboxContentRealm())
             }
         }
     }
@@ -110,7 +112,7 @@ class SearchUtils @Inject constructor(
      * @param filterFolder The selected Folder on which we filter the Search.
      */
     suspend fun convertRemoteThreadsToSearchThreads(remoteThreads: List<Thread>, filterFolder: Folder?): List<Thread> {
-        val cachedFolderNames = mutableMapOf<String, String>()
+        val cachedNamedFolders = mutableMapOf<String, NamedFolder>()
         return remoteThreads.map { remoteThread ->
             currentCoroutineContext().ensureActive()
 
@@ -118,7 +120,7 @@ class SearchUtils @Inject constructor(
                 isFromSearch = true
                 setFolderId(filterFolder)
                 keepOldMessagesData(filterFolder, mailboxContentRealm())
-                sharedThreadProcessing(appContext, cachedFolderNames, realm = mailboxContentRealm())
+                sharedThreadProcessing(appContext, cachedNamedFolders, realm = mailboxContentRealm())
             }
         }
     }
@@ -165,15 +167,26 @@ class SearchUtils @Inject constructor(
  * Thread processing that applies to both search threads from the api and from realm. Be careful, it relies on
  * [Thread.folderId] being set correctly.
  */
-private fun Thread.sharedThreadProcessing(context: Context, cachedFolderNames: MutableMap<String, String>, realm: Realm) {
-    setFolderName(cachedFolderNames, realm, context)
+private fun Thread.sharedThreadProcessing(context: Context, cachedNamedFolders: MutableMap<String, NamedFolder>, realm: Realm) {
+    setFolderName(cachedNamedFolders, realm, context)
 }
 
-private fun Thread.setFolderName(cachedFolderNames: MutableMap<String, String>, realm: Realm, context: Context) {
-    val computedFolderName = cachedFolderNames[folderId]
+private fun Thread.setFolderName(cachedNamedFolders: MutableMap<String, NamedFolder>, realm: Realm, context: Context) {
+    val cachedNamedFolder = cachedNamedFolders[folderId]
         ?: FolderController.getFolder(folderId, realm)
-            ?.getLocalizedName(context)
-            ?.also { cachedFolderNames[folderId] = it }
+            ?.let { NamedFolder.fromFolder(it, context) }
+            ?.also { cachedNamedFolders[folderId] = it }
 
-    computedFolderName?.let { folderName = it }
+    cachedNamedFolder?.let { namedFolder ->
+        folderName = namedFolder.computeDisplayedName(thread = this, context)
+    }
+}
+
+// If the remote folder id is inbox and the thread is snoozed, instead of using its default name, change it to snooze
+private fun NamedFolder.computeDisplayedName(thread: Thread, context: Context): String {
+    return if (this is NamedFolder.Role && role == FolderRole.INBOX && thread.isSnoozed()) {
+        context.getString(FolderRole.SNOOZED.folderNameRes)
+    } else {
+        getName(context)
+    }
 }


Reviews:
user: login: tevincent
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T10:46:02Z
user: login: tevincent
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T12:34:10Z

Review comments:
diff_hunk: @@ -0,0 +1,39 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
+ */
+package com.infomaniak.mail.ui.main.search
+
+import android.content.Context
+import com.infomaniak.mail.data.models.Folder
+
+sealed interface NamedFolder {
+    data class Role(val role: Folder.FolderRole) : NamedFolder {
+        override fun getName(context: Context): String = context.getString(role.folderNameRes)
+    }
+
+    data class Custom(val name: String) : NamedFolder {
+        override fun getName(context: Context): String = name
+    }
+
+    fun getName(context: Context): String
+
+    companion object {
+        fun fromFolder(folder: Folder, context: Context): NamedFolder = folder.role.let { role ->
+            if (role != null) Role(role) else Custom(folder.getLocalizedName(context))
+        }
+    }
path: app/src/main/java/com/infomaniak/mail/ui/main/search/NamedFolder.kt
user: login: tevincent
body: ```suggestion
    companion object {
        fun Folder.getName(context: Context): NamedFolder = role.let { role ->
            if (role != null) Role(role) else Custom(getLocalizedName(context))
        }
    }
```
created_at: 2025-05-07T10:38:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -165,15 +167,24 @@ class SearchUtils @Inject constructor(
  * Thread processing that applies to both search threads from the api and from realm. Be careful, it relies on
  * [Thread.folderId] being set correctly.
  */
-private fun Thread.sharedThreadProcessing(context: Context, cachedFolderNames: MutableMap<String, String>, realm: Realm) {
-    setFolderName(cachedFolderNames, realm, context)
+private fun Thread.sharedThreadProcessing(context: Context, cachedNamedFolders: MutableMap<String, NamedFolder>, realm: Realm) {
+    setFolderName(cachedNamedFolders, realm, context)
 }
 
-private fun Thread.setFolderName(cachedFolderNames: MutableMap<String, String>, realm: Realm, context: Context) {
-    val computedFolderName = cachedFolderNames[folderId]
+private fun Thread.setFolderName(cachedNamedFolders: MutableMap<String, NamedFolder>, realm: Realm, context: Context) {
+    val cachedNamedFolder = cachedNamedFolders[folderId]
         ?: FolderController.getFolder(folderId, realm)
-            ?.getLocalizedName(context)
-            ?.also { cachedFolderNames[folderId] = it }
+            ?.let { NamedFolder.fromFolder(it, context) }
+            ?.also { cachedNamedFolders[folderId] = it }
+
+    // If the remote folder id is inbox and the thread is snoozed, instead of using its default name, change it to snooze
+    val computedFolderName = cachedNamedFolder?.let {
+        if (it is NamedFolder.Role && it.role == FolderRole.INBOX && isSnoozed()) {
+            context.getString(FolderRole.SNOOZED.folderNameRes)
+        } else {
+            it.getName(context)
+        }
+    }
path: app/src/main/java/com/infomaniak/mail/utils/SearchUtils.kt
user: login: tevincent
body: ```kotlin
private fun Thread.setFolderName2(cachedNamedFolders: MutableMap<String, NamedFolder>, realm: Realm, context: Context) {

    fun getCachedNamedFolder(folderId: String): NamedFolder? {
        return cachedNamedFolders[folderId]
            ?: FolderController.getFolder(folderId, realm)?.getName(context)
                ?.also { cachedNamedFolders[folderId] = it }
    }

    // If the remote folder id is inbox and the thread is snoozed, instead of using its default name, change it to snooze
    getCachedNamedFolder(folderId)?.let {
        folderName = if (it is NamedFolder.Role && it.role == FolderRole.INBOX && isSnoozed()) {
            context.getString(FolderRole.SNOOZED.folderNameRes)
        } else {
            it.getName(context)
        }
    }
}
```
created_at: 2025-05-07T10:45:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-06T13:18:33Z
body: This PR/issue depends on:
* ~~Infomaniak/android-kMail#2347~~
By **[Dependent Issues](https://github.com/z0al/dependent-issues)** (🤖). Happy coding!
<!-- By Dependent Issues (Action) - DO NOT REMOVE -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T13:18:33Z
body: This PR/issue depends on:
* ~~Infomaniak/android-kMail#2347~~
By **[Dependent Issues](https://github.com/z0al/dependent-issues)** (🤖). Happy coding!
<!-- By Dependent Issues (Action) - DO NOT REMOVE -->
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-07T12:33:32Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Infomaniak_android-mail&pullRequest=2353) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0 New issues](https://sonarcloud.io/project/issues?id=Infomaniak_android-mail&pullRequest=2353&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted.svg '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Infomaniak_android-mail&pullRequest=2353&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Infomaniak_android-mail&pullRequest=2353&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data.svg '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Infomaniak_android-mail&pullRequest=2353&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Infomaniak_android-mail&pullRequest=2353)


author_association: NONE