Repository: thundernest/k-9
Issue: Duplicate notifications for new messages

### Checklist

- [X] I have used the search function to see if someone else has already submitted the same bug report.
- [X] I will describe the problem with as much detail as possible.

### App version

6.000

### Where did you get the app from?

Google Play

### Android version

11

### Device model

Moto g10

### Steps to reproduce

Part of the trigger seems to be only closing the notifications about a few of the received mails and the  receiving more mails. More I didn't find out in the last months.

### Expected behavior

If n mails arrive I normally get notifications with all their title lines 

### Actual behavior

Rarely I get n notifications with the title lone of one of my mails instead

### Logs

_No response_

Changed files:
app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt, app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt

diff --git a/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt b/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt
index db99f4034..de9f8c94b 100644
--- a/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt
+++ b/app/core/src/main/java/com/fsck/k9/notification/NotificationDataStore.kt
@@ -108,32 +108,43 @@ internal class NotificationDataStore {
         account: Account,
         selector: (List<MessageReference>) -> List<MessageReference>
     ): RemoveNotificationsResult? {
-        val notificationData = getNotificationData(account)
+        var notificationData = getNotificationData(account)
         if (notificationData.isEmpty()) return null
 
         val removeMessageReferences = selector.invoke(notificationData.messageReferences)
+        if (removeMessageReferences.isEmpty()) return null
 
         val operations = mutableListOf<NotificationStoreOperation>()
         val newNotificationHolders = mutableListOf<NotificationHolder>()
         val cancelNotificationIds = mutableListOf<Int>()
 
-        for (messageReference in removeMessageReferences) {
-            val notificationHolder = notificationData.activeNotifications.firstOrNull {
-                it.content.messageReference == messageReference
+        val activeMessageReferences = notificationData.activeNotifications.map { it.content.messageReference }.toSet()
+        val (removeActiveMessageReferences, removeInactiveMessageReferences) = removeMessageReferences
+            .partition { it in activeMessageReferences }
+
+        if (removeInactiveMessageReferences.isNotEmpty()) {
+            val inactiveMessageReferences = notificationData.inactiveNotifications
+                .map { it.content.messageReference }.toSet()
+
+            for (messageReference in removeInactiveMessageReferences) {
+                if (messageReference in inactiveMessageReferences) {
+                    operations.add(NotificationStoreOperation.Remove(messageReference))
+                }
             }
 
-            if (notificationHolder == null) {
-                val inactiveNotificationHolder = notificationData.inactiveNotifications.firstOrNull {
-                    it.content.messageReference == messageReference
-                } ?: continue
+            val removeMessageReferenceSet = removeInactiveMessageReferences.toSet()
+            notificationData = notificationData.copy(
+                inactiveNotifications = notificationData.inactiveNotifications
+                    .filter { it.content.messageReference !in removeMessageReferenceSet }
+            )
+        }
 
-                operations.add(NotificationStoreOperation.Remove(messageReference))
+        for (messageReference in removeActiveMessageReferences) {
+            val notificationHolder = notificationData.activeNotifications.first {
+                it.content.messageReference == messageReference
+            }
 
-                val newNotificationData = notificationData.copy(
-                    inactiveNotifications = notificationData.inactiveNotifications - inactiveNotificationHolder
-                )
-                notificationDataMap[account.uuid] = newNotificationData
-            } else if (notificationData.inactiveNotifications.isNotEmpty()) {
+            if (notificationData.inactiveNotifications.isNotEmpty()) {
                 val newNotificationHolder = notificationData.inactiveNotifications.first()
                     .toNotificationHolder(notificationHolder.notificationId)
 
@@ -148,29 +159,29 @@ internal class NotificationDataStore {
                     )
                 )
 
-                val newNotificationData = notificationData.copy(
+                notificationData = notificationData.copy(
                     activeNotifications = notificationData.activeNotifications - notificationHolder +
                         newNotificationHolder,
                     inactiveNotifications = notificationData.inactiveNotifications.drop(1)
                 )
-                notificationDataMap[account.uuid] = newNotificationData
             } else {
                 cancelNotificationIds.add(notificationHolder.notificationId)
 
                 operations.add(NotificationStoreOperation.Remove(messageReference))
 
-                val newNotificationData = notificationData.copy(
+                notificationData = notificationData.copy(
                     activeNotifications = notificationData.activeNotifications - notificationHolder
                 )
-                notificationDataMap[account.uuid] = newNotificationData
             }
         }
 
+        notificationDataMap[account.uuid] = notificationData
+
         return if (operations.isEmpty()) {
             null
         } else {
             RemoveNotificationsResult(
-                notificationData = getNotificationData(account),
+                notificationData = notificationData,
                 notificationStoreOperations = operations,
                 notificationHolders = newNotificationHolders,
                 cancelNotificationIds = cancelNotificationIds
diff --git a/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt b/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt
index 57ffc8aba..c3000f771 100644
--- a/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt
+++ b/app/core/src/test/java/com/fsck/k9/notification/NotificationDataStoreTest.kt
@@ -92,6 +92,38 @@ class NotificationDataStoreTest : RobolectricTest() {
         }
     }
 
+    @Test
+    fun `remove multiple notifications`() {
+        repeat(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1) { index ->
+            notificationDataStore.addNotification(account, createNotificationContent(index.toString()), TIMESTAMP)
+        }
+
+        val result = notificationDataStore.removeNotifications(account) { it.dropLast(1) }
+
+        assertNotNull(result) { removeResult ->
+            assertThat(removeResult.notificationData.newMessagesCount).isEqualTo(1)
+            assertThat(removeResult.cancelNotificationIds).hasSize(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS)
+        }
+    }
+
+    @Test
+    fun `remove all notifications`() {
+        repeat(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1) { index ->
+            notificationDataStore.addNotification(account, createNotificationContent(index.toString()), TIMESTAMP)
+        }
+
+        val result = notificationDataStore.removeNotifications(account) { it }
+
+        assertNotNull(result) { removeResult ->
+            assertThat(removeResult.notificationData.newMessagesCount).isEqualTo(0)
+            assertThat(removeResult.notificationHolders).hasSize(0)
+            assertThat(removeResult.notificationStoreOperations).hasSize(MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1)
+            for (notificationStoreOperation in removeResult.notificationStoreOperations) {
+                assertThat(notificationStoreOperation).isInstanceOf(NotificationStoreOperation.Remove::class.java)
+            }
+        }
+    }
+
     @Test
     fun testRemoveDoesNotLeakNotificationIds() {
         for (i in 1..MAX_NUMBER_OF_NEW_MESSAGE_NOTIFICATIONS + 1) {