diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index 5fa89c3..a77fc82 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -180,9 +180,8 @@ internal class FeatureModules internal constructor( val profileModule by unsafeLazy { ProfileModule(chatEngineModule.engine, trackingModule.errorTracker) } val notificationsModule by unsafeLazy { NotificationsModule( + chatEngineModule.engine, imageLoaderModule.iconLoader(), - storeModule.value.roomStore(), - storeModule.value.overviewStore(), context, intentFactory = coreAndroidModule.intentFactory(), dispatchers = coroutineDispatchers, diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 605e552..fd88d3d 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -13,6 +13,9 @@ interface ChatEngine : TaskRunner { fun invites(): Flow fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow + fun notificationsMessages(): Flow + fun notificationsInvites(): Flow + suspend fun login(request: LoginRequest): LoginResult suspend fun me(forceRefresh: Boolean): Me @@ -63,3 +66,17 @@ interface PushHandler { fun onNewToken(payload: JsonString) fun onMessageReceived(eventId: EventId?, roomId: RoomId?) } + +typealias UnreadNotifications = Pair>, NotificationDiff> + +data class NotificationDiff( + val unchanged: Map>, + val changedOrNew: Map>, + val removed: Map>, + val newRooms: Set +) + +data class InviteNotification( + val content: String, + val roomId: RoomId +) \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/fixture/NotificationDiffFixtures.kt b/chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt similarity index 89% rename from features/notifications/src/test/kotlin/fixture/NotificationDiffFixtures.kt rename to chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt index 7b9f0e9..bd50723 100644 --- a/features/notifications/src/test/kotlin/fixture/NotificationDiffFixtures.kt +++ b/chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt @@ -2,7 +2,7 @@ package fixture import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId -import app.dapk.st.notifications.NotificationDiff +import app.dapk.st.engine.NotificationDiff object NotificationDiffFixtures { diff --git a/features/notifications/build.gradle b/features/notifications/build.gradle index e579217..b7d1171 100644 --- a/features/notifications/build.gradle +++ b/features/notifications/build.gradle @@ -1,6 +1,7 @@ applyAndroidLibraryModule(project) dependencies { + implementation project(":chat-engine") implementation project(':domains:store') implementation project(":domains:android:work") implementation project(':domains:android:push') @@ -10,12 +11,13 @@ dependencies { implementation project(":features:messenger") implementation project(":features:navigator") + implementation Dependencies.mavenCentral.kotlinSerializationJson kotlinTest(it) androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":matrix:common")) - androidImportFixturesWorkaround(project, project(":matrix:services:sync")) + androidImportFixturesWorkaround(project, project(":chat-engine")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) } \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt index 503e074..43b7a9f 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt @@ -4,9 +4,9 @@ import android.app.Notification import android.content.Context import app.dapk.st.core.DeviceMeta import app.dapk.st.core.whenPOrHigher +import app.dapk.st.engine.RoomOverview import app.dapk.st.imageloader.IconLoader import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.navigator.IntentFactory import java.time.Clock @@ -87,7 +87,7 @@ class NotificationFactory( ) } - fun createInvite(inviteNotification: InviteNotification): AndroidNotification { + fun createInvite(inviteNotification: app.dapk.st.engine.InviteNotification): AndroidNotification { val openAppIntent = intentFactory.notificationOpenApp(context) return AndroidNotification( channelId = INVITE_CHANNEL_ID, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt index 8987ea9..63fe6d1 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt @@ -10,7 +10,7 @@ class NotificationInviteRenderer( private val androidNotificationBuilder: AndroidNotificationBuilder, ) { - fun render(inviteNotification: InviteNotification) { + fun render(inviteNotification: app.dapk.st.engine.InviteNotification) { notificationManager.notify( inviteNotification.roomId.value, INVITE_NOTIFICATION_ID, @@ -18,7 +18,7 @@ class NotificationInviteRenderer( ) } - private fun InviteNotification.toAndroidNotification() = androidNotificationBuilder.build( + private fun app.dapk.st.engine.InviteNotification.toAndroidNotification() = androidNotificationBuilder.build( notificationFactory.createInvite(this) ) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt index 1a12ddb..ab97978 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt @@ -5,9 +5,9 @@ import app.dapk.st.core.AppLogTag import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.extensions.ifNull import app.dapk.st.core.log +import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.RoomOverview import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import kotlinx.coroutines.withContext private const val SUMMARY_NOTIFICATION_ID = 101 diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStyleFactory.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStyleFactory.kt index 232c273..4cfb12b 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStyleFactory.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStyleFactory.kt @@ -3,8 +3,8 @@ package app.dapk.st.notifications import android.annotation.SuppressLint import app.dapk.st.core.DeviceMeta import app.dapk.st.core.whenPOrHigher +import app.dapk.st.engine.RoomOverview import app.dapk.st.imageloader.IconLoader -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.notifications.AndroidNotificationStyle.Inbox import app.dapk.st.notifications.AndroidNotificationStyle.Messaging diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt index 3368110..5c87c93 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt @@ -5,16 +5,14 @@ import android.content.Context import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.DeviceMeta import app.dapk.st.core.ProvidableModule +import app.dapk.st.engine.ChatEngine import app.dapk.st.imageloader.IconLoader -import app.dapk.st.matrix.sync.OverviewStore -import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.navigator.IntentFactory import java.time.Clock class NotificationsModule( + private val chatEngine: ChatEngine, private val iconLoader: IconLoader, - private val roomStore: RoomStore, - private val overviewStore: OverviewStore, private val context: Context, private val intentFactory: IntentFactory, private val dispatchers: CoroutineDispatchers, @@ -40,10 +38,9 @@ class NotificationsModule( ) return RenderNotificationsUseCase( notificationRenderer = notificationMessageRenderer, - observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore), notificationChannels = NotificationChannels(notificationManager), - observeInviteNotificationsUseCase = ObserveInviteNotificationsUseCaseImpl(overviewStore), - inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder) + inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder), + chatEngine = chatEngine, ) } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt index d51f6e8..097b0c6 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt @@ -1,7 +1,9 @@ package app.dapk.st.notifications -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview +import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.NotificationDiff +import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.RoomOverview import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -9,18 +11,17 @@ import kotlinx.coroutines.flow.onEach class RenderNotificationsUseCase( private val notificationRenderer: NotificationMessageRenderer, private val inviteRenderer: NotificationInviteRenderer, - private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase, - private val observeInviteNotificationsUseCase: ObserveInviteNotificationsUseCase, + private val chatEngine: ChatEngine, private val notificationChannels: NotificationChannels, ) { suspend fun listenForNotificationChanges(scope: CoroutineScope) { notificationChannels.initChannels() - observeRenderableUnreadEventsUseCase() + chatEngine.notificationsMessages() .onEach { (each, diff) -> renderUnreadChange(each, diff) } .launchIn(scope) - observeInviteNotificationsUseCase() + chatEngine.notificationsInvites() .onEach { inviteRenderer.render(it) } .launchIn(scope) } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt index 9f5c05d..49ec278 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt @@ -1,7 +1,7 @@ package app.dapk.st.notifications +import app.dapk.st.engine.RoomEvent import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.sync.RoomEvent class RoomEventsToNotifiableMapper { diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt index 014cce6..69bc836 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt @@ -137,7 +137,7 @@ class NotificationFactoryTest { fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) val content = "Content message" val result = notificationFactory.createInvite( - InviteNotification( + app.dapk.st.engine.InviteNotification( content = content, A_ROOM_ID, ) diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt index 5175956..c4a30ba 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt @@ -1,5 +1,6 @@ package app.dapk.st.notifications +import app.dapk.st.engine.UnreadNotifications import fake.* import fixture.NotificationDiffFixtures.aNotificationDiff import kotlinx.coroutines.test.TestScope @@ -19,12 +20,12 @@ class RenderNotificationsUseCaseTest { private val fakeNotificationChannels = FakeNotificationChannels().also { it.instance.expect { it.initChannels() } } + private val fakeChatEngine = FakeChatEngine() private val renderNotificationsUseCase = RenderNotificationsUseCase( fakeNotificationMessageRenderer.instance, fakeNotificationInviteRenderer.instance, - fakeObserveUnreadNotificationsUseCase, - fakeObserveInviteNotificationsUseCase, + fakeChatEngine, fakeNotificationChannels.instance, ) diff --git a/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt b/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt index 288d694..c5fbab4 100644 --- a/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt +++ b/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt @@ -2,14 +2,14 @@ package fake import app.dapk.st.notifications.NotificationMessageRenderer import app.dapk.st.notifications.NotificationState -import app.dapk.st.notifications.UnreadNotifications +import app.dapk.st.engine.UnreadNotifications import io.mockk.coVerify import io.mockk.mockk class FakeNotificationMessageRenderer { val instance = mockk() - fun verifyRenders(vararg unreadNotifications: UnreadNotifications) { + fun verifyRenders(vararg unreadNotifications: app.dapk.st.engine.UnreadNotifications) { unreadNotifications.forEach { unread -> coVerify { instance.render( diff --git a/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt b/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt index fba079f..5e1b7eb 100644 --- a/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt +++ b/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt @@ -1,10 +1,10 @@ package fake -import app.dapk.st.notifications.ObserveInviteNotificationsUseCase +import app.dapk.st.engine.ObserveInviteNotificationsUseCase import io.mockk.coEvery import io.mockk.mockk import test.delegateEmit -class FakeObserveInviteNotificationsUseCase : ObserveInviteNotificationsUseCase by mockk() { +class FakeObserveInviteNotificationsUseCase : app.dapk.st.engine.ObserveInviteNotificationsUseCase by mockk() { fun given() = coEvery { this@FakeObserveInviteNotificationsUseCase.invoke() }.delegateEmit() } \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt b/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt index dd881d2..b90b035 100644 --- a/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt +++ b/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt @@ -1,10 +1,10 @@ package fake -import app.dapk.st.notifications.ObserveUnreadNotificationsUseCase +import app.dapk.st.engine.ObserveUnreadNotificationsUseCase import io.mockk.coEvery import io.mockk.mockk import test.delegateEmit -class FakeObserveUnreadNotificationsUseCase : ObserveUnreadNotificationsUseCase by mockk() { +class FakeObserveUnreadNotificationsUseCase : app.dapk.st.engine.ObserveUnreadNotificationsUseCase by mockk() { fun given() = coEvery { this@FakeObserveUnreadNotificationsUseCase.invoke() }.delegateEmit() } \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index 3494ee9..71981fc 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -41,6 +41,8 @@ class MatrixEngine internal constructor( private val matrixMediaDecrypter: Lazy, private val matrixPushHandler: Lazy, private val inviteUseCase: Lazy, + private val notificationMessagesUseCase: Lazy, + private val notificationInvitesUseCase: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() @@ -50,6 +52,14 @@ class MatrixEngine internal constructor( return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts) } + override fun notificationsMessages(): Flow { + return notificationMessagesUseCase.value.invoke() + } + + override fun notificationsInvites(): Flow { + return notificationInvitesUseCase.value.invoke() + } + override suspend fun login(request: LoginRequest): LoginResult { return matrix.value.authService().login(request.engine()).engine() } @@ -190,6 +200,8 @@ class MatrixEngine internal constructor( mediaDecrypter, pushHandler, invitesUseCase, + unsafeLazy { ObserveUnreadNotificationsUseCaseImpl(roomStore) }, + unsafeLazy { ObserveInviteNotificationsUseCaseImpl(overviewStore) }, ) } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt similarity index 82% rename from features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt index 67d88c5..b926f75 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt @@ -1,17 +1,16 @@ -package app.dapk.st.notifications +package app.dapk.st.engine -import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.sync.InviteMeta import app.dapk.st.matrix.sync.OverviewStore import app.dapk.st.matrix.sync.RoomInvite import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* -internal typealias ObserveInviteNotificationsUseCase = suspend () -> Flow +internal typealias ObserveInviteNotificationsUseCase = () -> Flow class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase { - override suspend fun invoke(): Flow { + override fun invoke(): Flow { return overviewStore.latestInvites() .diff() .drop(1) @@ -43,8 +42,3 @@ class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewS private fun Flow>.flatten() = this.flatMapConcat { items -> flow { items.forEach { this.emit(it) } } } - -data class InviteNotification( - val content: String, - val roomId: RoomId -) \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt similarity index 76% rename from features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt index 8a3860d..83f2981 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt @@ -1,4 +1,4 @@ -package app.dapk.st.notifications +package app.dapk.st.engine import app.dapk.st.core.AppLogTag import app.dapk.st.core.extensions.clearAndPutAll @@ -6,17 +6,16 @@ import app.dapk.st.core.extensions.containsKey import app.dapk.st.core.log import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.matrix.sync.RoomStore import kotlinx.coroutines.flow.* +import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent +import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview -typealias UnreadNotifications = Pair>, NotificationDiff> -internal typealias ObserveUnreadNotificationsUseCase = suspend () -> Flow +internal typealias ObserveUnreadNotificationsUseCase = () -> Flow class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase { - override suspend fun invoke(): Flow { + override fun invoke(): Flow { return roomStore.observeUnread() .mapWithDiff() .avoidShowingPreviousNotificationsOnLaunch() @@ -25,28 +24,7 @@ class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : } -private fun Flow.onlyRenderableChanges(): Flow { - val inferredCurrentNotifications = mutableMapOf>() - return this - .filter { (_, diff) -> - when { - diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> { - log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes") - false - } - - inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> { - log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read") - false - } - - else -> true - } - } - .onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) } -} - -private fun Flow>>.mapWithDiff(): Flow>, NotificationDiff>> { +private fun Flow>>.mapWithDiff(): Flow>, NotificationDiff>> { val previousUnreadEvents = mutableMapOf>() return this.map { each -> val allUnreadIds = each.toTimestampedIds() @@ -83,19 +61,39 @@ private fun Map>?.toLatestTimestamps() = this?. private fun Map>.toEventIds() = this.mapValues { it.value.map { it.first } } -private fun Map>.toTimestampedIds() = this +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 List.toEventIds() = this.map { it.eventId to it.utcTimestamp } private fun Flow.avoidShowingPreviousNotificationsOnLaunch() = drop(1) -data class NotificationDiff( - val unchanged: Map>, - val changedOrNew: Map>, - val removed: Map>, - val newRooms: Set -) +private fun Flow>, NotificationDiff>>.onlyRenderableChanges(): Flow { + val inferredCurrentNotifications = mutableMapOf>() + return this + .filter { (_, diff) -> + when { + diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> { + log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes") + false + } + + inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> { + log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read") + false + } + + else -> true + } + } + .onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) } + .map { + val engineModels = it.first + .mapKeys { it.key.engine() } + .mapValues { it.value.map { it.engine() } } + engineModels to it.second + } +} typealias TimestampedEventId = Pair \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt similarity index 69% rename from features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt rename to matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt index 1a932fe..bc77daa 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt @@ -1,24 +1,27 @@ -package app.dapk.st.notifications +package app.dapk.st.engine -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import fake.FakeRoomStore import fixture.NotificationDiffFixtures.aNotificationDiff +import fixture.aMatrixRoomOverview 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 import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent +import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview -private val NO_UNREADS = emptyMap>() +private val NO_UNREADS = emptyMap>() 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")) +private val A_ROOM_OVERVIEW = aMatrixRoomOverview(roomId = aRoomId("1")) +private val A_ROOM_OVERVIEW_2 = aMatrixRoomOverview(roomId = aRoomId("2")) + +private fun MatrixRoomOverview.withUnreads(vararg events: MatrixRoomEvent) = mapOf(this to events.toList()) +private fun MatrixRoomOverview.toDiff(vararg events: MatrixRoomEvent) = mapOf(this.roomId to events.map { it.eventId }) class ObserveUnreadRenderNotificationsUseCaseTest { @@ -33,7 +36,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest { val result = useCase.invoke().toList() result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), newRooms = setOf(A_ROOM_OVERVIEW.roomId) ) @@ -47,11 +50,11 @@ class ObserveUnreadRenderNotificationsUseCaseTest { val result = useCase.invoke().toList() result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( 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_2)) + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) ) } @@ -64,7 +67,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_2)) + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) ) } @@ -92,7 +95,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest { val result = useCase.invoke().toList() result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), newRooms = setOf(A_ROOM_OVERVIEW.roomId) ), @@ -110,8 +113,10 @@ class ObserveUnreadRenderNotificationsUseCaseTest { result shouldBeEqualTo emptyList() } - private fun givenNoInitialUnreads(vararg unreads: Map>) = fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads)) + private fun givenNoInitialUnreads(vararg unreads: Map>) = + fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads)) } -private fun RoomOverview.withUnreads(vararg events: RoomEvent) = mapOf(this to events.toList()) -private fun RoomOverview.toDiff(vararg events: RoomEvent) = mapOf(this.roomId to events.map { it.eventId }) +private fun Map>.engine() = this + .mapKeys { it.key.engine() } + .mapValues { it.value.map { it.engine() } } diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt index ecc363c..03c019f 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt @@ -7,7 +7,7 @@ import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.sync.LastMessage import app.dapk.st.matrix.sync.RoomOverview -fun aRoomOverview( +fun aMatrixRoomOverview( roomId: RoomId = aRoomId(), roomCreationUtc: Long = 0L, roomName: String? = null, diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt index 260e46d..dbb646d 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt @@ -5,6 +5,6 @@ import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.matrix.sync.RoomState fun aRoomState( - roomOverview: RoomOverview = aRoomOverview(), + roomOverview: RoomOverview = aMatrixRoomOverview(), events: List = listOf(aRoomMessageEvent()), ) = RoomState(roomOverview, events) \ No newline at end of file