porting message observing to the chat engine
This commit is contained in:
parent
d2e8a29af8
commit
baf7cfc17a
|
@ -177,11 +177,8 @@ internal class FeatureModules internal constructor(
|
||||||
}
|
}
|
||||||
val messengerModule by unsafeLazy {
|
val messengerModule by unsafeLazy {
|
||||||
MessengerModule(
|
MessengerModule(
|
||||||
matrixModules.sync,
|
matrixModules.engine,
|
||||||
matrixModules.message,
|
matrixModules.message,
|
||||||
matrixModules.room,
|
|
||||||
storeModule.value.credentialsStore(),
|
|
||||||
storeModule.value.roomStore(),
|
|
||||||
clock,
|
clock,
|
||||||
context,
|
context,
|
||||||
base64,
|
base64,
|
||||||
|
|
|
@ -10,6 +10,8 @@ interface ChatEngine {
|
||||||
|
|
||||||
fun invites(): Flow<InviteState>
|
fun invites(): Flow<InviteState>
|
||||||
|
|
||||||
|
suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState>
|
||||||
|
|
||||||
suspend fun login(request: LoginRequest): LoginResult
|
suspend fun login(request: LoginRequest): LoginResult
|
||||||
|
|
||||||
suspend fun me(forceRefresh: Boolean): Me
|
suspend fun me(forceRefresh: Boolean): Me
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package app.dapk.st.engine
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.*
|
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<DirectoryItem>
|
typealias DirectoryState = List<DirectoryItem>
|
||||||
typealias OverviewState = List<RoomOverview>
|
typealias OverviewState = List<RoomOverview>
|
||||||
|
@ -79,3 +83,118 @@ sealed interface ImportResult {
|
||||||
|
|
||||||
data class Update(val importedKeysCount: Long) : 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<RoomEvent>,
|
||||||
|
)
|
||||||
|
|
||||||
|
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<String, String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ applyAndroidComposeLibraryModule(project)
|
||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(":chat-engine")
|
||||||
implementation project(":matrix:services:sync")
|
implementation project(":matrix:services:sync")
|
||||||
implementation project(":matrix:services:message")
|
implementation project(":matrix:services:message")
|
||||||
implementation project(":matrix:services:crypto")
|
implementation project(":matrix:services:crypto")
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import app.dapk.st.core.Base64
|
import app.dapk.st.core.Base64
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
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.CredentialsStore
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
|
@ -14,11 +15,8 @@ import app.dapk.st.matrix.sync.SyncService
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
|
||||||
class MessengerModule(
|
class MessengerModule(
|
||||||
private val syncService: SyncService,
|
private val chatEngine: ChatEngine,
|
||||||
private val messageService: MessageService,
|
private val messageService: MessageService,
|
||||||
private val roomService: RoomService,
|
|
||||||
private val credentialsStore: CredentialsStore,
|
|
||||||
private val roomStore: RoomStore,
|
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val base64: Base64,
|
private val base64: Base64,
|
||||||
|
@ -28,11 +26,8 @@ class MessengerModule(
|
||||||
|
|
||||||
internal fun messengerViewModel(): MessengerViewModel {
|
internal fun messengerViewModel(): MessengerViewModel {
|
||||||
return MessengerViewModel(
|
return MessengerViewModel(
|
||||||
|
chatEngine,
|
||||||
messageService,
|
messageService,
|
||||||
roomService,
|
|
||||||
roomStore,
|
|
||||||
credentialsStore,
|
|
||||||
timelineUseCase(),
|
|
||||||
LocalIdFactory(),
|
LocalIdFactory(),
|
||||||
imageMetaReader,
|
imageMetaReader,
|
||||||
messageOptionsStore,
|
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)
|
internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, base64, roomId)
|
||||||
}
|
}
|
|
@ -44,12 +44,12 @@ import app.dapk.st.core.StartObserving
|
||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
import app.dapk.st.core.extensions.takeIfContent
|
import app.dapk.st.core.extensions.takeIfContent
|
||||||
import app.dapk.st.design.components.*
|
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.RoomId
|
||||||
import app.dapk.st.matrix.common.UserId
|
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.messenger.gallery.ImageGalleryActivityPayload
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
import app.dapk.st.navigator.Navigator
|
import app.dapk.st.navigator.Navigator
|
||||||
|
@ -196,7 +196,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions
|
||||||
AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) {
|
AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
|
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
|
||||||
is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
|
is RoomEvent.Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
|
||||||
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
|
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -482,7 +482,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
when (val replyingTo = content.message.replyingTo) {
|
when (val replyingTo = content.message.replyingTo) {
|
||||||
is Message -> {
|
is RoomEvent.Message -> {
|
||||||
Text(
|
Text(
|
||||||
text = replyingTo.content,
|
text = replyingTo.content,
|
||||||
color = content.textColor().copy(alpha = 0.8f),
|
color = content.textColor().copy(alpha = 0.8f),
|
||||||
|
@ -525,7 +525,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (val message = content.message.message) {
|
when (val message = content.message.message) {
|
||||||
is Message -> {
|
is RoomEvent.Message -> {
|
||||||
Text(
|
Text(
|
||||||
text = message.content,
|
text = message.content,
|
||||||
color = content.textColor(),
|
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(12.dp)) {
|
||||||
Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) {
|
Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package app.dapk.st.messenger
|
package app.dapk.st.messenger
|
||||||
|
|
||||||
import app.dapk.st.core.Lce
|
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.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
|
|
||||||
data class MessengerScreenState(
|
data class MessengerScreenState(
|
||||||
|
|
|
@ -4,31 +4,24 @@ import androidx.lifecycle.viewModelScope
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.core.extensions.takeIfContent
|
import app.dapk.st.core.extensions.takeIfContent
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.engine.RoomEvent
|
||||||
import app.dapk.st.matrix.common.RoomId
|
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.MessageService
|
||||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
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.navigator.MessageAttachment
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import app.dapk.st.viewmodel.MutableStateFactory
|
import app.dapk.st.viewmodel.MutableStateFactory
|
||||||
import app.dapk.st.viewmodel.defaultStateFactory
|
import app.dapk.st.viewmodel.defaultStateFactory
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
|
||||||
internal class MessengerViewModel(
|
internal class MessengerViewModel(
|
||||||
|
private val chatEngine: ChatEngine,
|
||||||
private val messageService: MessageService,
|
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 localIdFactory: LocalIdFactory,
|
||||||
private val imageContentReader: ImageContentReader,
|
private val imageContentReader: ImageContentReader,
|
||||||
private val messageOptionsStore: MessageOptionsStore,
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
|
@ -83,29 +76,10 @@ internal class MessengerViewModel(
|
||||||
|
|
||||||
private fun start(action: MessengerAction.OnMessengerVisible) {
|
private fun start(action: MessengerAction.OnMessengerVisible) {
|
||||||
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) }
|
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) }
|
||||||
syncJob = viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
roomStore.markRead(action.roomId)
|
syncJob = chatEngine.messages(action.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
|
||||||
|
.onEach { updateState { copy(roomState = Lce.Content(it)) } }
|
||||||
val credentials = credentialsStore.credentials()!!
|
.launchIn(this)
|
||||||
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<Unit> {
|
|
||||||
return async {
|
|
||||||
runCatching {
|
|
||||||
roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = messageOptionsStore.isReadReceiptsDisabled())
|
|
||||||
roomStore.markRead(state.roomState.roomOverview.roomId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,12 +164,6 @@ internal class MessengerViewModel(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
|
|
||||||
.filterIsInstance<RoomEvent.Message>()
|
|
||||||
.filterNot { it.author.id == self }
|
|
||||||
.firstOrNull()
|
|
||||||
?.eventId
|
|
||||||
|
|
||||||
sealed interface MessengerAction {
|
sealed interface MessengerAction {
|
||||||
data class ComposerTextUpdate(val newValue: String) : MessengerAction
|
data class ComposerTextUpdate(val newValue: String) : MessengerAction
|
||||||
data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction
|
data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction
|
||||||
|
|
|
@ -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.EventId
|
||||||
import app.dapk.st.matrix.common.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
import app.dapk.st.matrix.message.MessageService
|
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) {
|
internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package app.dapk.st.engine
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal class LocalIdFactory {
|
||||||
|
fun create() = "local.${UUID.randomUUID()}"
|
||||||
|
}
|
|
@ -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.crypto.ImportResult as MatrixImportResult
|
||||||
import app.dapk.st.matrix.room.ProfileService.Me as MatrixMe
|
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.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.RoomInvite as MatrixRoomInvite
|
||||||
import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview
|
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
|
import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing as MatrixTyping
|
||||||
|
|
||||||
fun MatrixRoomOverview.engine() = RoomOverview(
|
fun MatrixRoomOverview.engine() = RoomOverview(
|
||||||
|
@ -76,4 +79,38 @@ fun MatrixImportResult.engine() = when (this) {
|
||||||
|
|
||||||
is MatrixImportResult.Success -> ImportResult.Success(this.roomIds, this.totalImportedKeysCount)
|
is MatrixImportResult.Success -> ImportResult.Success(this.roomIds, this.totalImportedKeysCount)
|
||||||
is MatrixImportResult.Update -> ImportResult.Update(this.importedKeysCount)
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -36,6 +36,7 @@ import java.time.Clock
|
||||||
class MatrixEngine internal constructor(
|
class MatrixEngine internal constructor(
|
||||||
private val directoryUseCase: Lazy<DirectoryUseCase>,
|
private val directoryUseCase: Lazy<DirectoryUseCase>,
|
||||||
private val matrix: Lazy<MatrixClient>,
|
private val matrix: Lazy<MatrixClient>,
|
||||||
|
private val timelineUseCase: Lazy<ReadMarkingTimeline>,
|
||||||
) : ChatEngine {
|
) : ChatEngine {
|
||||||
|
|
||||||
override fun directory() = directoryUseCase.value.state()
|
override fun directory() = directoryUseCase.value.state()
|
||||||
|
@ -43,6 +44,10 @@ class MatrixEngine internal constructor(
|
||||||
return matrix.value.syncService().invites().map { it.map { it.engine() } }
|
return matrix.value.syncService().invites().map { it.map { it.engine() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState> {
|
||||||
|
return timelineUseCase.value.foo(roomId, isReadReceiptsDisabled = disableReadReceipts)
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
@ -116,8 +121,14 @@ class MatrixEngine internal constructor(
|
||||||
roomStore
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.EventId
|
||||||
import app.dapk.st.matrix.common.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
import app.dapk.st.matrix.message.MessageService
|
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<MessageService.LocalEcho>) -> RoomState
|
internal typealias MergeWithLocalEchosUseCase = (RoomState, RoomMember, List<MessageService.LocalEcho>) -> RoomState
|
||||||
|
|
|
@ -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<MessengerState> {
|
||||||
|
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<RoomEvent.Message>()
|
||||||
|
.filterNot { it.author.id == self }
|
||||||
|
.firstOrNull()
|
||||||
|
?.eventId
|
|
@ -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.core.extensions.startAndIgnoreEmissions
|
||||||
import app.dapk.st.matrix.common.RoomId
|
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.common.UserId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.matrix.room.RoomService
|
import app.dapk.st.matrix.room.RoomService
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerState>
|
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerState>
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ internal class TimelineUseCaseImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId },
|
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(),
|
||||||
self = userId,
|
self = userId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -45,14 +45,8 @@ internal class TimelineUseCaseImpl(
|
||||||
|
|
||||||
private fun roomDatasource(roomId: RoomId) = combine(
|
private fun roomDatasource(roomId: RoomId) = combine(
|
||||||
syncService.startSyncing().startAndIgnoreEmissions(),
|
syncService.startSyncing().startAndIgnoreEmissions(),
|
||||||
syncService.room(roomId)
|
syncService.room(roomId).map { it.engine() }
|
||||||
) { _, room -> room }
|
) { _, room -> room }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null)
|
private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null)
|
||||||
|
|
||||||
data class MessengerState(
|
|
||||||
val self: UserId,
|
|
||||||
val roomState: RoomState,
|
|
||||||
val typing: SyncService.SyncEvent.Typing?
|
|
||||||
)
|
|
Loading…
Reference in New Issue