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.
This commit is contained in:
hayaksi1
2026-08-09 15:09:59 +03:00
parent 044737a3a7
commit cbe555009b
2 changed files with 89 additions and 2 deletions
@@ -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<NotifiableMessageEvent>,
): List<NotifiableMessageEvent> {
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<NotifiableMessageEvent>,
imageLoader: ImageLoader,
@@ -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<NotifiableMessageEvent>,
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<String?> {
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,