From cbe555009b1f6fbc216cd4193fb741e94425d94c Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:09:59 +0300 Subject: [PATCH] Stop repeating messages that are already shown in a room notification A room notification is rebuilt from the one currently on screen: its messaging style is extracted and the incoming events are appended. Every event was appended unconditionally, so an event re-delivered while its notification was still showing was added again, and the notification re-alerted each time. Skip events whose id is already present in the existing messaging style, and set onlyAlertOnce when the batch adds nothing new. Smart reply failures are exempt, because that path deliberately reuses the same synthesised event id to append the "message not sent" line. Filtering is per event rather than per message so an image and its caption stay a unit. This fixes the visible duplication, not the re-delivery that triggers it, and does not help once the notification has been dismissed. --- .../factories/NotificationCreator.kt | 14 +++- .../DefaultNotificationCreatorTest.kt | 77 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index 44a9b5ca30..afbbe4af4f 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -196,11 +196,12 @@ class DefaultNotificationCreator( isThread = threadId != null, roomIsGroup = !roomInfo.isDm, ) - messagingStyle.addMessagesFromEvents(events, imageLoader) + val newEvents = messagingStyle.filterOutAlreadyDisplayedEvents(events) + messagingStyle.addMessagesFromEvents(newEvents, imageLoader) return builder .setCategory(category) .setNumber(events.size) - .setOnlyAlertOnce(roomInfo.isUpdated) + .setOnlyAlertOnce(roomInfo.isUpdated || newEvents.isEmpty()) .setWhen(lastMessageTimestamp) // MESSAGING_STYLE sets title and content for API 16 and above devices. .setStyle(messagingStyle) @@ -416,6 +417,15 @@ class DefaultNotificationCreator( .build() } + private fun MessagingStyle.filterOutAlreadyDisplayedEvents( + events: List, + ): List { + val displayedEventIds = messages.mapNotNullTo(mutableSetOf()) { it.extras.getString(MESSAGE_EVENT_ID) } + return events.filter { event -> + event.isSmartReplyError() || displayedEventIds.add(event.eventId.value) + } + } + private suspend fun MessagingStyle.addMessagesFromEvents( events: List, imageLoader: ImageLoader, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index 38b387a180..299c5ad213 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -19,6 +19,7 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.A_COLOR_INT import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -40,6 +41,7 @@ import io.element.android.libraries.push.impl.notifications.factories.action.Rej import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider @@ -302,6 +304,81 @@ class DefaultNotificationCreatorTest : RobolectricTest() { result.commonAssertions() } + @Test + fun `test createMessagesListNotification does not add an event which is already displayed`() = runTest { + val sut = createNotificationCreator() + val event = aNotifiableMessageEvent(body = "A message") + val existingNotification = sut.createRoomNotification(events = listOf(event)) + assertThat(existingNotification.flags and Notification.FLAG_ONLY_ALERT_ONCE).isEqualTo(0) + val result = sut.createRoomNotification(events = listOf(event), existingNotification = existingNotification) + assertThat(result.messageTexts()).containsExactly("A message") + assertThat(result.flags and Notification.FLAG_ONLY_ALERT_ONCE).isNotEqualTo(0) + } + + @Test + fun `test createMessagesListNotification adds a new event to the already displayed ones`() = runTest { + val sut = createNotificationCreator() + val existingNotification = sut.createRoomNotification(events = listOf(aNotifiableMessageEvent(body = "A message"))) + val result = sut.createRoomNotification( + events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID_2, body = "Another message")), + existingNotification = existingNotification, + ) + assertThat(result.messageTexts()).containsExactly("A message", "Another message").inOrder() + } + + @Test + fun `test createMessagesListNotification adds the smart reply error of an already displayed event`() = runTest { + val sut = createNotificationCreator() + val event = aNotifiableMessageEvent(body = "A message").copy(outGoingMessage = true) + val existingNotification = sut.createRoomNotification(events = listOf(event)) + val result = sut.createRoomNotification( + events = listOf(event.copy(outGoingMessageFailed = true)), + existingNotification = existingNotification, + ) + assertThat(result.messageTexts()).containsExactly("A message", "test").inOrder() + } + + @Test + fun `test createMessagesListNotification does not add an image and its caption twice`() = runTest { + val sut = createNotificationCreator() + val event = aNotifiableMessageEvent(body = "A caption").copy( + imageUriString = "aUri", + imageMimeType = "image/jpeg", + ) + val existingNotification = sut.createRoomNotification(events = listOf(event)) + assertThat(existingNotification.messageTexts()).containsExactly("test", "A caption").inOrder() + val result = sut.createRoomNotification(events = listOf(event), existingNotification = existingNotification) + assertThat(result.messageTexts()).containsExactly("test", "A caption").inOrder() + } + + private suspend fun NotificationCreator.createRoomNotification( + events: List, + existingNotification: Notification? = null, + ) = createMessagesListNotification( + notificationAccountParams = aNotificationAccountParams(), + roomInfo = RoomEventGroupInfo( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + roomDisplayName = "roomDisplayName", + hasSmartReplyError = false, + shouldBing = false, + customSound = null, + isUpdated = false, + ), + threadId = null, + largeIcon = null, + lastMessageTimestamp = 123_456L, + tickerText = "tickerText", + existingNotification = existingNotification, + imageLoader = FakeImageLoader(), + events = events, + ) + + private fun Notification.messageTexts(): List { + val messagingStyle = checkNotNull(NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(this)) + return messagingStyle.messages.map { it.text?.toString() } + } + private fun Notification.commonAssertions( expectedGroup: String? = aMatrixUser().userId.value, expectedCategory: String? = NotificationCompat.CATEGORY_MESSAGE,