porting notifications to chat engine

This commit is contained in:
Adam Brown 2022-10-12 20:52:02 +01:00
parent 86ad2a8a32
commit 4d033230e4
22 changed files with 122 additions and 96 deletions

View File

@ -180,9 +180,8 @@ internal class FeatureModules internal constructor(
val profileModule by unsafeLazy { ProfileModule(chatEngineModule.engine, trackingModule.errorTracker) } val profileModule by unsafeLazy { ProfileModule(chatEngineModule.engine, trackingModule.errorTracker) }
val notificationsModule by unsafeLazy { val notificationsModule by unsafeLazy {
NotificationsModule( NotificationsModule(
chatEngineModule.engine,
imageLoaderModule.iconLoader(), imageLoaderModule.iconLoader(),
storeModule.value.roomStore(),
storeModule.value.overviewStore(),
context, context,
intentFactory = coreAndroidModule.intentFactory(), intentFactory = coreAndroidModule.intentFactory(),
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,

View File

@ -13,6 +13,9 @@ interface ChatEngine : TaskRunner {
fun invites(): Flow<InviteState> fun invites(): Flow<InviteState>
fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState> fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState>
fun notificationsMessages(): Flow<UnreadNotifications>
fun notificationsInvites(): Flow<InviteNotification>
suspend fun login(request: LoginRequest): LoginResult suspend fun login(request: LoginRequest): LoginResult
suspend fun me(forceRefresh: Boolean): Me suspend fun me(forceRefresh: Boolean): Me
@ -63,3 +66,17 @@ interface PushHandler {
fun onNewToken(payload: JsonString) fun onNewToken(payload: JsonString)
fun onMessageReceived(eventId: EventId?, roomId: RoomId?) fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
} }
typealias UnreadNotifications = Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>
data class NotificationDiff(
val unchanged: Map<RoomId, List<EventId>>,
val changedOrNew: Map<RoomId, List<EventId>>,
val removed: Map<RoomId, List<EventId>>,
val newRooms: Set<RoomId>
)
data class InviteNotification(
val content: String,
val roomId: RoomId
)

View File

@ -2,7 +2,7 @@ package fixture
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.notifications.NotificationDiff import app.dapk.st.engine.NotificationDiff
object NotificationDiffFixtures { object NotificationDiffFixtures {

View File

@ -1,6 +1,7 @@
applyAndroidLibraryModule(project) applyAndroidLibraryModule(project)
dependencies { dependencies {
implementation project(":chat-engine")
implementation project(':domains:store') implementation project(':domains:store')
implementation project(":domains:android:work") implementation project(":domains:android:work")
implementation project(':domains:android:push') implementation project(':domains:android:push')
@ -10,12 +11,13 @@ dependencies {
implementation project(":features:messenger") implementation project(":features:messenger")
implementation project(":features:navigator") implementation project(":features:navigator")
implementation Dependencies.mavenCentral.kotlinSerializationJson implementation Dependencies.mavenCentral.kotlinSerializationJson
kotlinTest(it) kotlinTest(it)
androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":matrix:services:sync")) androidImportFixturesWorkaround(project, project(":chat-engine"))
androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":domains:android:stub"))
} }

View File

@ -4,9 +4,9 @@ import android.app.Notification
import android.content.Context import android.content.Context
import app.dapk.st.core.DeviceMeta import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.whenPOrHigher import app.dapk.st.core.whenPOrHigher
import app.dapk.st.engine.RoomOverview
import app.dapk.st.imageloader.IconLoader import app.dapk.st.imageloader.IconLoader
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.IntentFactory
import java.time.Clock 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) val openAppIntent = intentFactory.notificationOpenApp(context)
return AndroidNotification( return AndroidNotification(
channelId = INVITE_CHANNEL_ID, channelId = INVITE_CHANNEL_ID,

View File

@ -10,7 +10,7 @@ class NotificationInviteRenderer(
private val androidNotificationBuilder: AndroidNotificationBuilder, private val androidNotificationBuilder: AndroidNotificationBuilder,
) { ) {
fun render(inviteNotification: InviteNotification) { fun render(inviteNotification: app.dapk.st.engine.InviteNotification) {
notificationManager.notify( notificationManager.notify(
inviteNotification.roomId.value, inviteNotification.roomId.value,
INVITE_NOTIFICATION_ID, 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) notificationFactory.createInvite(this)
) )

View File

@ -5,9 +5,9 @@ import app.dapk.st.core.AppLogTag
import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.extensions.ifNull import app.dapk.st.core.extensions.ifNull
import app.dapk.st.core.log 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.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
private const val SUMMARY_NOTIFICATION_ID = 101 private const val SUMMARY_NOTIFICATION_ID = 101

View File

@ -3,8 +3,8 @@ package app.dapk.st.notifications
import android.annotation.SuppressLint import android.annotation.SuppressLint
import app.dapk.st.core.DeviceMeta import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.whenPOrHigher import app.dapk.st.core.whenPOrHigher
import app.dapk.st.engine.RoomOverview
import app.dapk.st.imageloader.IconLoader 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.Inbox
import app.dapk.st.notifications.AndroidNotificationStyle.Messaging import app.dapk.st.notifications.AndroidNotificationStyle.Messaging

View File

@ -5,16 +5,14 @@ import android.content.Context
import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.DeviceMeta import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.ProvidableModule import app.dapk.st.core.ProvidableModule
import app.dapk.st.engine.ChatEngine
import app.dapk.st.imageloader.IconLoader 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 app.dapk.st.navigator.IntentFactory
import java.time.Clock import java.time.Clock
class NotificationsModule( class NotificationsModule(
private val chatEngine: ChatEngine,
private val iconLoader: IconLoader, private val iconLoader: IconLoader,
private val roomStore: RoomStore,
private val overviewStore: OverviewStore,
private val context: Context, private val context: Context,
private val intentFactory: IntentFactory, private val intentFactory: IntentFactory,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
@ -40,10 +38,9 @@ class NotificationsModule(
) )
return RenderNotificationsUseCase( return RenderNotificationsUseCase(
notificationRenderer = notificationMessageRenderer, notificationRenderer = notificationMessageRenderer,
observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore),
notificationChannels = NotificationChannels(notificationManager), notificationChannels = NotificationChannels(notificationManager),
observeInviteNotificationsUseCase = ObserveInviteNotificationsUseCaseImpl(overviewStore), inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder),
inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder) chatEngine = chatEngine,
) )
} }

View File

@ -1,7 +1,9 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.sync.RoomOverview 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.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -9,18 +11,17 @@ import kotlinx.coroutines.flow.onEach
class RenderNotificationsUseCase( class RenderNotificationsUseCase(
private val notificationRenderer: NotificationMessageRenderer, private val notificationRenderer: NotificationMessageRenderer,
private val inviteRenderer: NotificationInviteRenderer, private val inviteRenderer: NotificationInviteRenderer,
private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase, private val chatEngine: ChatEngine,
private val observeInviteNotificationsUseCase: ObserveInviteNotificationsUseCase,
private val notificationChannels: NotificationChannels, private val notificationChannels: NotificationChannels,
) { ) {
suspend fun listenForNotificationChanges(scope: CoroutineScope) { suspend fun listenForNotificationChanges(scope: CoroutineScope) {
notificationChannels.initChannels() notificationChannels.initChannels()
observeRenderableUnreadEventsUseCase() chatEngine.notificationsMessages()
.onEach { (each, diff) -> renderUnreadChange(each, diff) } .onEach { (each, diff) -> renderUnreadChange(each, diff) }
.launchIn(scope) .launchIn(scope)
observeInviteNotificationsUseCase() chatEngine.notificationsInvites()
.onEach { inviteRenderer.render(it) } .onEach { inviteRenderer.render(it) }
.launchIn(scope) .launchIn(scope)
} }

View File

@ -1,7 +1,7 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import app.dapk.st.engine.RoomEvent
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.sync.RoomEvent
class RoomEventsToNotifiableMapper { class RoomEventsToNotifiableMapper {

View File

@ -137,7 +137,7 @@ class NotificationFactoryTest {
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
val content = "Content message" val content = "Content message"
val result = notificationFactory.createInvite( val result = notificationFactory.createInvite(
InviteNotification( app.dapk.st.engine.InviteNotification(
content = content, content = content,
A_ROOM_ID, A_ROOM_ID,
) )

View File

@ -1,5 +1,6 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import app.dapk.st.engine.UnreadNotifications
import fake.* import fake.*
import fixture.NotificationDiffFixtures.aNotificationDiff import fixture.NotificationDiffFixtures.aNotificationDiff
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -19,12 +20,12 @@ class RenderNotificationsUseCaseTest {
private val fakeNotificationChannels = FakeNotificationChannels().also { private val fakeNotificationChannels = FakeNotificationChannels().also {
it.instance.expect { it.initChannels() } it.instance.expect { it.initChannels() }
} }
private val fakeChatEngine = FakeChatEngine()
private val renderNotificationsUseCase = RenderNotificationsUseCase( private val renderNotificationsUseCase = RenderNotificationsUseCase(
fakeNotificationMessageRenderer.instance, fakeNotificationMessageRenderer.instance,
fakeNotificationInviteRenderer.instance, fakeNotificationInviteRenderer.instance,
fakeObserveUnreadNotificationsUseCase, fakeChatEngine,
fakeObserveInviteNotificationsUseCase,
fakeNotificationChannels.instance, fakeNotificationChannels.instance,
) )

View File

@ -2,14 +2,14 @@ package fake
import app.dapk.st.notifications.NotificationMessageRenderer import app.dapk.st.notifications.NotificationMessageRenderer
import app.dapk.st.notifications.NotificationState 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.coVerify
import io.mockk.mockk import io.mockk.mockk
class FakeNotificationMessageRenderer { class FakeNotificationMessageRenderer {
val instance = mockk<NotificationMessageRenderer>() val instance = mockk<NotificationMessageRenderer>()
fun verifyRenders(vararg unreadNotifications: UnreadNotifications) { fun verifyRenders(vararg unreadNotifications: app.dapk.st.engine.UnreadNotifications) {
unreadNotifications.forEach { unread -> unreadNotifications.forEach { unread ->
coVerify { coVerify {
instance.render( instance.render(

View File

@ -1,10 +1,10 @@
package fake package fake
import app.dapk.st.notifications.ObserveInviteNotificationsUseCase import app.dapk.st.engine.ObserveInviteNotificationsUseCase
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import test.delegateEmit import test.delegateEmit
class FakeObserveInviteNotificationsUseCase : ObserveInviteNotificationsUseCase by mockk() { class FakeObserveInviteNotificationsUseCase : app.dapk.st.engine.ObserveInviteNotificationsUseCase by mockk() {
fun given() = coEvery { this@FakeObserveInviteNotificationsUseCase.invoke() }.delegateEmit() fun given() = coEvery { this@FakeObserveInviteNotificationsUseCase.invoke() }.delegateEmit()
} }

View File

@ -1,10 +1,10 @@
package fake package fake
import app.dapk.st.notifications.ObserveUnreadNotificationsUseCase import app.dapk.st.engine.ObserveUnreadNotificationsUseCase
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import test.delegateEmit import test.delegateEmit
class FakeObserveUnreadNotificationsUseCase : ObserveUnreadNotificationsUseCase by mockk() { class FakeObserveUnreadNotificationsUseCase : app.dapk.st.engine.ObserveUnreadNotificationsUseCase by mockk() {
fun given() = coEvery { this@FakeObserveUnreadNotificationsUseCase.invoke() }.delegateEmit() fun given() = coEvery { this@FakeObserveUnreadNotificationsUseCase.invoke() }.delegateEmit()
} }

View File

@ -41,6 +41,8 @@ class MatrixEngine internal constructor(
private val matrixMediaDecrypter: Lazy<MatrixMediaDecrypter>, private val matrixMediaDecrypter: Lazy<MatrixMediaDecrypter>,
private val matrixPushHandler: Lazy<MatrixPushHandler>, private val matrixPushHandler: Lazy<MatrixPushHandler>,
private val inviteUseCase: Lazy<InviteUseCase>, private val inviteUseCase: Lazy<InviteUseCase>,
private val notificationMessagesUseCase: Lazy<ObserveUnreadNotificationsUseCase>,
private val notificationInvitesUseCase: Lazy<ObserveInviteNotificationsUseCase>,
) : ChatEngine { ) : ChatEngine {
override fun directory() = directoryUseCase.value.state() override fun directory() = directoryUseCase.value.state()
@ -50,6 +52,14 @@ class MatrixEngine internal constructor(
return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts) return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts)
} }
override fun notificationsMessages(): Flow<UnreadNotifications> {
return notificationMessagesUseCase.value.invoke()
}
override fun notificationsInvites(): Flow<InviteNotification> {
return notificationInvitesUseCase.value.invoke()
}
override suspend fun login(request: LoginRequest): LoginResult { override suspend fun login(request: LoginRequest): LoginResult {
return matrix.value.authService().login(request.engine()).engine() return matrix.value.authService().login(request.engine()).engine()
} }
@ -190,6 +200,8 @@ class MatrixEngine internal constructor(
mediaDecrypter, mediaDecrypter,
pushHandler, pushHandler,
invitesUseCase, invitesUseCase,
unsafeLazy { ObserveUnreadNotificationsUseCaseImpl(roomStore) },
unsafeLazy { ObserveInviteNotificationsUseCaseImpl(overviewStore) },
) )
} }

View File

@ -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.InviteMeta
import app.dapk.st.matrix.sync.OverviewStore import app.dapk.st.matrix.sync.OverviewStore
import app.dapk.st.matrix.sync.RoomInvite import app.dapk.st.matrix.sync.RoomInvite
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
internal typealias ObserveInviteNotificationsUseCase = suspend () -> Flow<InviteNotification> internal typealias ObserveInviteNotificationsUseCase = () -> Flow<InviteNotification>
class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase { class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase {
override suspend fun invoke(): Flow<InviteNotification> { override fun invoke(): Flow<InviteNotification> {
return overviewStore.latestInvites() return overviewStore.latestInvites()
.diff() .diff()
.drop(1) .drop(1)
@ -43,8 +42,3 @@ class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewS
private fun <T> Flow<Set<T>>.flatten() = this.flatMapConcat { items -> private fun <T> Flow<Set<T>>.flatten() = this.flatMapConcat { items ->
flow { items.forEach { this.emit(it) } } flow { items.forEach { this.emit(it) } }
} }
data class InviteNotification(
val content: String,
val roomId: RoomId
)

View File

@ -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.AppLogTag
import app.dapk.st.core.extensions.clearAndPutAll 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.core.log
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId 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 app.dapk.st.matrix.sync.RoomStore
import kotlinx.coroutines.flow.* 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<Map<RoomOverview, List<RoomEvent>>, NotificationDiff> internal typealias ObserveUnreadNotificationsUseCase = () -> Flow<UnreadNotifications>
internal typealias ObserveUnreadNotificationsUseCase = suspend () -> Flow<UnreadNotifications>
class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase { class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase {
override suspend fun invoke(): Flow<UnreadNotifications> { override fun invoke(): Flow<UnreadNotifications> {
return roomStore.observeUnread() return roomStore.observeUnread()
.mapWithDiff() .mapWithDiff()
.avoidShowingPreviousNotificationsOnLaunch() .avoidShowingPreviousNotificationsOnLaunch()
@ -25,28 +24,7 @@ class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) :
} }
private fun Flow<UnreadNotifications>.onlyRenderableChanges(): Flow<UnreadNotifications> { private fun Flow<Map<MatrixRoomOverview, List<MatrixRoomEvent>>>.mapWithDiff(): Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>> {
val inferredCurrentNotifications = mutableMapOf<RoomId, List<RoomEvent>>()
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<Map<RoomOverview, List<RoomEvent>>>.mapWithDiff(): Flow<Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>> {
val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>() val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>()
return this.map { each -> return this.map { each ->
val allUnreadIds = each.toTimestampedIds() val allUnreadIds = each.toTimestampedIds()
@ -83,19 +61,39 @@ private fun Map<RoomId, List<TimestampedEventId>>?.toLatestTimestamps() = this?.
private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } } private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } }
private fun Map<RoomOverview, List<RoomEvent>>.toTimestampedIds() = this private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.toTimestampedIds() = this
.mapValues { it.value.toEventIds() } .mapValues { it.value.toEventIds() }
.mapKeys { it.key.roomId } .mapKeys { it.key.roomId }
private fun List<RoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp } private fun List<MatrixRoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp }
private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1) private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1)
data class NotificationDiff( private fun Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>>.onlyRenderableChanges(): Flow<UnreadNotifications> {
val unchanged: Map<RoomId, List<EventId>>, val inferredCurrentNotifications = mutableMapOf<RoomId, List<MatrixRoomEvent>>()
val changedOrNew: Map<RoomId, List<EventId>>, return this
val removed: Map<RoomId, List<EventId>>, .filter { (_, diff) ->
val newRooms: Set<RoomId> 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<EventId, Long> typealias TimestampedEventId = Pair<EventId, Long>

View File

@ -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 fake.FakeRoomStore
import fixture.NotificationDiffFixtures.aNotificationDiff import fixture.NotificationDiffFixtures.aNotificationDiff
import fixture.aMatrixRoomOverview
import fixture.aRoomId import fixture.aRoomId
import fixture.aRoomMessageEvent import fixture.aRoomMessageEvent
import fixture.aRoomOverview
import fixture.anEventId import fixture.anEventId
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test 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<RoomOverview, List<RoomEvent>>() private val NO_UNREADS = emptyMap<MatrixRoomOverview, List<MatrixRoomEvent>>()
private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000) 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_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000)
private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1")) private val A_ROOM_OVERVIEW = aMatrixRoomOverview(roomId = aRoomId("1"))
private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2")) 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 { class ObserveUnreadRenderNotificationsUseCaseTest {
@ -33,7 +36,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
val result = useCase.invoke().toList() val result = useCase.invoke().toList()
result shouldBeEqualTo listOf( 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), changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId) newRooms = setOf(A_ROOM_OVERVIEW.roomId)
) )
@ -47,11 +50,11 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
val result = useCase.invoke().toList() val result = useCase.invoke().toList()
result shouldBeEqualTo listOf( 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), changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId) 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() val result = useCase.invoke().toList()
result shouldBeEqualTo listOf( 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() val result = useCase.invoke().toList()
result shouldBeEqualTo listOf( 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), changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId) newRooms = setOf(A_ROOM_OVERVIEW.roomId)
), ),
@ -110,8 +113,10 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
result shouldBeEqualTo emptyList() result shouldBeEqualTo emptyList()
} }
private fun givenNoInitialUnreads(vararg unreads: Map<RoomOverview, List<RoomEvent>>) = fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads)) private fun givenNoInitialUnreads(vararg unreads: Map<MatrixRoomOverview, List<MatrixRoomEvent>>) =
fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads))
} }
private fun RoomOverview.withUnreads(vararg events: RoomEvent) = mapOf(this to events.toList()) private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.engine() = this
private fun RoomOverview.toDiff(vararg events: RoomEvent) = mapOf(this.roomId to events.map { it.eventId }) .mapKeys { it.key.engine() }
.mapValues { it.value.map { it.engine() } }

View File

@ -7,7 +7,7 @@ import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.sync.LastMessage import app.dapk.st.matrix.sync.LastMessage
import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.matrix.sync.RoomOverview
fun aRoomOverview( fun aMatrixRoomOverview(
roomId: RoomId = aRoomId(), roomId: RoomId = aRoomId(),
roomCreationUtc: Long = 0L, roomCreationUtc: Long = 0L,
roomName: String? = null, roomName: String? = null,

View File

@ -5,6 +5,6 @@ import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.RoomState
fun aRoomState( fun aRoomState(
roomOverview: RoomOverview = aRoomOverview(), roomOverview: RoomOverview = aMatrixRoomOverview(),
events: List<RoomEvent> = listOf(aRoomMessageEvent()), events: List<RoomEvent> = listOf(aRoomMessageEvent()),
) = RoomState(roomOverview, events) ) = RoomState(roomOverview, events)