From baf7cfc17acd708714c9a1689ab838297ada052f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 18:58:03 +0100 Subject: [PATCH] porting message observing to the chat engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 5 +- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 2 + .../main/kotlin/app/dapk/st/engine/Models.kt | 119 ++++++++++++++++++ features/messenger/build.gradle | 1 + .../app/dapk/st/messenger/MessengerModule.kt | 16 +-- .../app/dapk/st/messenger/MessengerScreen.kt | 16 +-- .../app/dapk/st/messenger/MessengerState.kt | 3 +- .../dapk/st/messenger/MessengerViewModel.kt | 52 ++------ .../app/dapk/st/engine}/LocalEchoMapper.kt | 4 +- .../app/dapk/st/engine/LocalIdFactory.kt | 7 ++ .../app/dapk/st/engine/MappingExtensions.kt | 37 ++++++ .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 13 +- .../st/engine}/MergeWithLocalEchosUseCase.kt | 4 +- .../app/dapk/st/engine/ReadMarkingTimeline.kt | 55 ++++++++ .../app/dapk/st/engine}/TimelineUseCase.kt | 14 +-- 15 files changed, 263 insertions(+), 85 deletions(-) rename {features/messenger/src/main/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/LocalEchoMapper.kt (95%) create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt rename {features/messenger/src/main/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/MergeWithLocalEchosUseCase.kt (94%) create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt rename {features/messenger/src/main/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/TimelineUseCase.kt (86%) 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 a1ea937..e9f20f1 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -177,11 +177,8 @@ internal class FeatureModules internal constructor( } val messengerModule by unsafeLazy { MessengerModule( - matrixModules.sync, + matrixModules.engine, matrixModules.message, - matrixModules.room, - storeModule.value.credentialsStore(), - storeModule.value.roomStore(), clock, context, base64, 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 f5e7fe8..76a89aa 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 @@ -10,6 +10,8 @@ interface ChatEngine { fun invites(): Flow + suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow + suspend fun login(request: LoginRequest): LoginResult suspend fun me(forceRefresh: Boolean): Me diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt index df5631a..fa975d1 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt @@ -1,6 +1,10 @@ package app.dapk.st.engine import app.dapk.st.matrix.common.* +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter typealias DirectoryState = List typealias OverviewState = List @@ -79,3 +83,118 @@ sealed interface ImportResult { data class Update(val importedKeysCount: Long) : ImportResult } + +data class MessengerState( + val self: UserId, + val roomState: RoomState, + val typing: Typing? +) + +data class RoomState( + val roomOverview: RoomOverview, + val events: List, +) + +internal val DEFAULT_ZONE = ZoneId.systemDefault() +internal val MESSAGE_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm") + +sealed class RoomEvent { + + abstract val eventId: EventId + abstract val utcTimestamp: Long + abstract val author: RoomMember + abstract val meta: MessageMeta + + data class Message( + override val eventId: EventId, + override val utcTimestamp: Long, + val content: String, + override val author: RoomMember, + override val meta: MessageMeta, + val edited: Boolean = false, + val redacted: Boolean = false, + ) : RoomEvent() { + + val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + } + + data class Reply( + val message: RoomEvent, + val replyingTo: RoomEvent, + ) : RoomEvent() { + + override val eventId: EventId = message.eventId + override val utcTimestamp: Long = message.utcTimestamp + override val author: RoomMember = message.author + override val meta: MessageMeta = message.meta + + val replyingToSelf = replyingTo.author == message.author + + val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + } + + data class Image( + override val eventId: EventId, + override val utcTimestamp: Long, + val imageMeta: ImageMeta, + override val author: RoomMember, + override val meta: MessageMeta, + val edited: Boolean = false, + ) : RoomEvent() { + + val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + + data class ImageMeta( + val width: Int?, + val height: Int?, + val url: String, + val keys: Keys?, + ) { + + data class Keys( + val k: String, + val iv: String, + val v: String, + val hashes: Map, + ) + + } + } + +} + +sealed class MessageMeta { + + object FromServer : MessageMeta() + + data class LocalEcho( + val echoId: String, + val state: State + ) : MessageMeta() { + + sealed class State { + object Sending : State() + + object Sent : State() + + data class Error( + val message: String, + val type: Type, + ) : State() { + + enum class Type { + UNKNOWN + } + } + } + } +} \ No newline at end of file diff --git a/features/messenger/build.gradle b/features/messenger/build.gradle index 9d31472..e2a2096 100644 --- a/features/messenger/build.gradle +++ b/features/messenger/build.gradle @@ -2,6 +2,7 @@ applyAndroidComposeLibraryModule(project) apply plugin: 'kotlin-parcelize' dependencies { + implementation project(":chat-engine") implementation project(":matrix:services:sync") implementation project(":matrix:services:message") implementation project(":matrix:services:crypto") diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt index 9b48609..292cfae 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt @@ -4,6 +4,7 @@ import android.content.Context import app.dapk.st.core.Base64 import app.dapk.st.core.ProvidableModule import app.dapk.st.domain.application.message.MessageOptionsStore +import app.dapk.st.engine.ChatEngine import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.message.MessageService @@ -14,11 +15,8 @@ import app.dapk.st.matrix.sync.SyncService import java.time.Clock class MessengerModule( - private val syncService: SyncService, + private val chatEngine: ChatEngine, private val messageService: MessageService, - private val roomService: RoomService, - private val credentialsStore: CredentialsStore, - private val roomStore: RoomStore, private val clock: Clock, private val context: Context, private val base64: Base64, @@ -28,11 +26,8 @@ class MessengerModule( internal fun messengerViewModel(): MessengerViewModel { return MessengerViewModel( + chatEngine, messageService, - roomService, - roomStore, - credentialsStore, - timelineUseCase(), LocalIdFactory(), imageMetaReader, messageOptionsStore, @@ -40,10 +35,5 @@ class MessengerModule( ) } - private fun timelineUseCase(): TimelineUseCaseImpl { - val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper())) - return TimelineUseCaseImpl(syncService, messageService, roomService, mergeWithLocalEchosUseCase) - } - internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, base64, roomId) } \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 5d1447e..b6673e0 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -44,12 +44,12 @@ import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.design.components.* +import app.dapk.st.engine.MessageMeta +import app.dapk.st.engine.MessengerState +import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.RoomState import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.sync.MessageMeta -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomEvent.Message -import app.dapk.st.matrix.sync.RoomState import app.dapk.st.messenger.gallery.ImageGalleryActivityPayload import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.Navigator @@ -196,7 +196,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) { when (item) { is RoomEvent.Image -> MessageImage(it as BubbleContent) - is Message -> TextBubbleContent(it as BubbleContent) + is RoomEvent.Message -> TextBubbleContent(it as BubbleContent) is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent) } } @@ -482,7 +482,7 @@ private fun ReplyBubbleContent(content: BubbleContent) { ) Spacer(modifier = Modifier.height(2.dp)) when (val replyingTo = content.message.replyingTo) { - is Message -> { + is RoomEvent.Message -> { Text( text = replyingTo.content, color = content.textColor().copy(alpha = 0.8f), @@ -525,7 +525,7 @@ private fun ReplyBubbleContent(content: BubbleContent) { ) } when (val message = content.message.message) { - is Message -> { + is RoomEvent.Message -> { Text( text = message.content, color = content.textColor(), @@ -642,7 +642,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un ) } ) { - if (it is Message) { + if (it is RoomEvent.Message) { Box(Modifier.padding(12.dp)) { Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) { Icon( diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt index 7c0259c..ce4d829 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt @@ -1,8 +1,9 @@ package app.dapk.st.messenger import app.dapk.st.core.Lce +import app.dapk.st.engine.MessengerState +import app.dapk.st.engine.RoomEvent import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.navigator.MessageAttachment data class MessengerScreenState( diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index d780da5..03ecc8f 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -4,31 +4,24 @@ import androidx.lifecycle.viewModelScope import app.dapk.st.core.Lce import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.domain.application.message.MessageOptionsStore -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.common.EventId +import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.RoomEvent import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.navigator.MessageAttachment import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.MutableStateFactory import app.dapk.st.viewmodel.defaultStateFactory -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import java.time.Clock internal class MessengerViewModel( + private val chatEngine: ChatEngine, private val messageService: MessageService, - private val roomService: RoomService, - private val roomStore: RoomStore, - private val credentialsStore: CredentialsStore, - private val observeTimeline: ObserveTimelineUseCase, private val localIdFactory: LocalIdFactory, private val imageContentReader: ImageContentReader, private val messageOptionsStore: MessageOptionsStore, @@ -83,29 +76,10 @@ internal class MessengerViewModel( private fun start(action: MessengerAction.OnMessengerVisible) { updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) } - syncJob = viewModelScope.launch { - roomStore.markRead(action.roomId) - - val credentials = credentialsStore.credentials()!! - var lastKnownReadEvent: EventId? = null - observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state -> - state.latestMessageEventFromOthers(self = credentials.userId)?.let { - if (lastKnownReadEvent != it) { - updateRoomReadStateAsync(latestReadEvent = it, state) - lastKnownReadEvent = it - } - } - updateState { copy(roomState = Lce.Content(state)) } - }.collect() - } - } - - private fun CoroutineScope.updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState): Deferred { - return async { - runCatching { - roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = messageOptionsStore.isReadReceiptsDisabled()) - roomStore.markRead(state.roomState.roomOverview.roomId) - } + viewModelScope.launch { + syncJob = chatEngine.messages(action.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled()) + .onEach { updateState { copy(roomState = Lce.Content(it)) } } + .launchIn(this) } } @@ -190,12 +164,6 @@ internal class MessengerViewModel( } -private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events - .filterIsInstance() - .filterNot { it.author.id == self } - .firstOrNull() - ?.eventId - sealed interface MessengerAction { data class ComposerTextUpdate(val newValue: String) : MessengerAction data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt similarity index 95% rename from features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt index 8fc2fa7..1880fcc 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt @@ -1,10 +1,8 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.sync.MessageMeta -import app.dapk.st.matrix.sync.RoomEvent internal class LocalEchoMapper(private val metaMapper: MetaMapper) { diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt new file mode 100644 index 0000000..57b37eb --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt @@ -0,0 +1,7 @@ +package app.dapk.st.engine + +import java.util.* + +internal class LocalIdFactory { + fun create() = "local.${UUID.randomUUID()}" +} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt index 8e61404..9317c81 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt @@ -7,8 +7,11 @@ import app.dapk.st.matrix.auth.AuthService.LoginResult as MatrixLoginResult import app.dapk.st.matrix.crypto.ImportResult as MatrixImportResult import app.dapk.st.matrix.room.ProfileService.Me as MatrixMe import app.dapk.st.matrix.sync.LastMessage as MatrixLastMessage +import app.dapk.st.matrix.sync.MessageMeta as MatrixMessageMeta +import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview +import app.dapk.st.matrix.sync.RoomState as MatrixRoomState import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing as MatrixTyping fun MatrixRoomOverview.engine() = RoomOverview( @@ -76,4 +79,38 @@ fun MatrixImportResult.engine() = when (this) { is MatrixImportResult.Success -> ImportResult.Success(this.roomIds, this.totalImportedKeysCount) is MatrixImportResult.Update -> ImportResult.Update(this.importedKeysCount) +} + +fun MatrixRoomState.engine() = RoomState( + this.roomOverview.engine(), + this.events.map { it.engine() } +) + +fun MatrixRoomEvent.engine(): RoomEvent = when (this) { + is MatrixRoomEvent.Image -> RoomEvent.Image(this.eventId, this.utcTimestamp, this.imageMeta.engine(), this.author, this.meta.engine(), this.edited) + is MatrixRoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, this.content, this.author, this.meta.engine(), this.edited, this.redacted) + is MatrixRoomEvent.Reply -> RoomEvent.Reply(this.message.engine(), this.replyingTo.engine()) +} + +fun MatrixRoomEvent.Image.ImageMeta.engine() = RoomEvent.Image.ImageMeta( + this.width, + this.height, + this.url, + this.keys?.let { RoomEvent.Image.ImageMeta.Keys(it.k, it.iv, it.v, it.hashes) } +) + +fun MatrixMessageMeta.engine() = when (this) { + MatrixMessageMeta.FromServer -> MessageMeta.FromServer + is MatrixMessageMeta.LocalEcho -> MessageMeta.LocalEcho( + this.echoId, when (val echo = this.state) { + is MatrixMessageMeta.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error( + echo.message, when (echo.type) { + MatrixMessageMeta.LocalEcho.State.Error.Type.UNKNOWN -> MessageMeta.LocalEcho.State.Error.Type.UNKNOWN + } + ) + + MatrixMessageMeta.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending + MatrixMessageMeta.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent + } + ) } \ 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 5fd7dda..b4f9e0e 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 @@ -36,6 +36,7 @@ import java.time.Clock class MatrixEngine internal constructor( private val directoryUseCase: Lazy, private val matrix: Lazy, + private val timelineUseCase: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() @@ -43,6 +44,10 @@ class MatrixEngine internal constructor( return matrix.value.syncService().invites().map { it.map { it.engine() } } } + override suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow { + return timelineUseCase.value.foo(roomId, isReadReceiptsDisabled = disableReadReceipts) + } + override suspend fun login(request: LoginRequest): LoginResult { return matrix.value.authService().login(request.engine()).engine() } @@ -116,8 +121,14 @@ class MatrixEngine internal constructor( roomStore ) } + val timelineUseCase = unsafeLazy { + val matrix = lazyMatrix.value + val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper())) + val timeline = TimelineUseCaseImpl(matrix.syncService(), matrix.messageService(), matrix.roomService(), mergeWithLocalEchosUseCase) + ReadMarkingTimeline(roomStore, credentialsStore, timeline, matrix.roomService()) + } - return MatrixEngine(directoryUseCase, lazyMatrix) + return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase) } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt similarity index 94% rename from features/messenger/src/main/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCase.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt index ba23d34..8f91848 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt @@ -1,10 +1,8 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomState internal typealias MergeWithLocalEchosUseCase = (RoomState, RoomMember, List) -> RoomState diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt new file mode 100644 index 0000000..ee1c641 --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt @@ -0,0 +1,55 @@ +package app.dapk.st.engine + +import app.dapk.st.matrix.common.CredentialsStore +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.UserId +import app.dapk.st.matrix.room.RoomService +import app.dapk.st.matrix.sync.RoomEvent +import app.dapk.st.matrix.sync.RoomStore +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach + +class ReadMarkingTimeline( + private val roomStore: RoomStore, + private val credentialsStore: CredentialsStore, + private val observeTimelineUseCase: ObserveTimelineUseCase, + private val roomService: RoomService, +) { + + suspend fun foo(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow { + var lastKnownReadEvent: EventId? = null + val credentials = credentialsStore.credentials()!! + roomStore.markRead(roomId) + return observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state -> + state.latestMessageEventFromOthers(self = credentials.userId)?.let { + if (lastKnownReadEvent != it) { + updateRoomReadStateAsync(latestReadEvent = it, state, isReadReceiptsDisabled) + lastKnownReadEvent = it + } + } + } + } + + private suspend fun updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState, isReadReceiptsDisabled: Boolean): Deferred<*> { + return coroutineScope { + async { + runCatching { + roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = isReadReceiptsDisabled) + roomStore.markRead(state.roomState.roomOverview.roomId) + } + } + } + } + +} + +private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events + .filterIsInstance() + .filterNot { it.author.id == self } + .firstOrNull() + ?.eventId diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt similarity index 86% rename from features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt index c1d9538..55b2f30 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt @@ -1,4 +1,4 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.core.extensions.startAndIgnoreEmissions import app.dapk.st.matrix.common.RoomId @@ -6,10 +6,10 @@ import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.SyncService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow @@ -37,7 +37,7 @@ internal class TimelineUseCaseImpl( ) } }, - typing = events.filterIsInstance().firstOrNull { it.roomId == roomId }, + typing = events.filterIsInstance().firstOrNull { it.roomId == roomId }?.engine(), self = userId, ) } @@ -45,14 +45,8 @@ internal class TimelineUseCaseImpl( private fun roomDatasource(roomId: RoomId) = combine( syncService.startSyncing().startAndIgnoreEmissions(), - syncService.room(roomId) + syncService.room(roomId).map { it.engine() } ) { _, room -> room } } private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null) - -data class MessengerState( - val self: UserId, - val roomState: RoomState, - val typing: SyncService.SyncEvent.Typing? -)