diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt index dd7f7dc..8a3860d 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt @@ -34,10 +34,12 @@ private fun Flow.onlyRenderableChanges(): Flow { log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read") false } + else -> true } } @@ -45,29 +47,48 @@ private fun Flow.onlyRenderableChanges(): Flow>>.mapWithDiff(): Flow>, NotificationDiff>> { - val previousUnreadEvents = mutableMapOf>() + val previousUnreadEvents = mutableMapOf>() return this.map { each -> - val allUnreadIds = each.toIds() + val allUnreadIds = each.toTimestampedIds() val notificationDiff = calculateDiff(allUnreadIds, previousUnreadEvents) previousUnreadEvents.clearAndPutAll(allUnreadIds) each to notificationDiff } } -private fun calculateDiff(allUnread: Map>, previousUnread: Map>?): NotificationDiff { +private fun calculateDiff(allUnread: Map>, previousUnread: Map>?): NotificationDiff { + val previousLatestEventTimestamps = previousUnread.toLatestTimestamps() val newRooms = allUnread.filter { !previousUnread.containsKey(it.key) }.keys - val unchanged = previousUnread?.filter { allUnread.containsKey(it.key) && it.value == allUnread[it.key] } ?: emptyMap() - val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) } + + val unchanged = previousUnread?.filter { + allUnread.containsKey(it.key) && (it.value == allUnread[it.key]) + } ?: emptyMap() + val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) }.mapValues { (key, value) -> + val isChangedRoom = !newRooms.contains(key) + if (isChangedRoom) { + val latest = previousLatestEventTimestamps[key] ?: 0L + value.filter { + val isExistingEvent = (previousUnread?.get(key)?.contains(it) ?: false) + !isExistingEvent && it.second > latest + } + } else { + value + } + }.filter { it.value.isNotEmpty() } val removed = previousUnread?.filter { !allUnread.containsKey(it.key) } ?: emptyMap() - return NotificationDiff(unchanged, changedOrNew, removed, newRooms) + return NotificationDiff(unchanged.toEventIds(), changedOrNew.toEventIds(), removed.toEventIds(), newRooms) } -private fun List.toEventIds() = this.map { it.eventId } +private fun Map>?.toLatestTimestamps() = this?.mapValues { it.value.maxOf { it.second } } ?: emptyMap() -private fun Map>.toIds() = this +private fun Map>.toEventIds() = this.mapValues { it.value.map { it.first } } + +private fun Map>.toTimestampedIds() = this .mapValues { it.value.toEventIds() } .mapKeys { it.key.roomId } +private fun List.toEventIds() = this.map { it.eventId to it.utcTimestamp } + private fun Flow.avoidShowingPreviousNotificationsOnLaunch() = drop(1) data class NotificationDiff( @@ -76,3 +97,5 @@ data class NotificationDiff( val removed: Map>, val newRooms: Set ) + +typealias TimestampedEventId = Pair \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt index fdce470..1a932fe 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt @@ -3,8 +3,11 @@ package app.dapk.st.notifications import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomOverview import fake.FakeRoomStore -import fixture.* import fixture.NotificationDiffFixtures.aNotificationDiff +import fixture.aRoomId +import fixture.aRoomMessageEvent +import fixture.aRoomOverview +import fixture.anEventId import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest @@ -12,8 +15,8 @@ import org.amshove.kluent.shouldBeEqualTo import org.junit.Test private val NO_UNREADS = emptyMap>() -private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello") -private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world") +private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000) +private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000) private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1")) private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2")) @@ -48,7 +51,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest { changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), newRooms = setOf(A_ROOM_OVERVIEW.roomId) ), - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE, A_MESSAGE_2)) + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) ) } @@ -61,7 +64,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest { val result = useCase.invoke().toList() result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE, A_MESSAGE_2)) + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) ) } @@ -76,6 +79,26 @@ class ObserveUnreadRenderNotificationsUseCaseTest { result shouldBeEqualTo emptyList() } + @Test + fun `given new and then historical message, when reading a message, then only emits the latest`() = runTest { + fakeRoomStore.givenUnreadEvents( + flowOf( + NO_UNREADS, + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE), + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE.copy(eventId = anEventId("old"), utcTimestamp = -1)) + ) + ) + + val result = useCase.invoke().toList() + + result shouldBeEqualTo listOf( + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( + changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), + newRooms = setOf(A_ROOM_OVERVIEW.roomId) + ), + ) + } + @Test fun `given initial unreads, when reading a duplicate unread, then emits nothing`() = runTest { fakeRoomStore.givenUnreadEvents(