adding image type parsing and handling

This commit is contained in:
Adam Brown 2022-03-30 22:33:24 +01:00
parent ed3752c7bc
commit 5a6b7cf1c0
21 changed files with 462 additions and 172 deletions

View File

@ -22,9 +22,10 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
}
}
fun RoomEvent.mergeWith(echo: MessageService.LocalEcho) = when (this) {
fun RoomEvent.mergeWith(echo: MessageService.LocalEcho): RoomEvent = when (this) {
is RoomEvent.Message -> this.copy(meta = metaMapper.toMeta(echo))
is RoomEvent.Reply -> this.copy(message = this.message.copy(meta = metaMapper.toMeta(echo)))
is RoomEvent.Reply -> this.copy(message = this.message.mergeWith(echo))
is RoomEvent.Image -> this.copy(meta = metaMapper.toMeta(echo))
}
}

View File

@ -36,6 +36,7 @@ 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.navigator.Navigator
import coil.compose.rememberImagePainter
import kotlinx.coroutines.launch
@Composable
@ -155,28 +156,70 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState) {
items = state.events,
key = { _, item -> item.eventId.value },
) { index, item ->
val previousEvent = if (index != 0) state.events[index - 1] else null
val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id
when (item) {
is Message -> {
val wasPreviousMessageSameSender = when (val previousEvent = if (index != 0) state.events[index - 1] else null) {
null -> false
is Message -> previousEvent.author.id == item.author.id
is RoomEvent.Reply -> previousEvent.message.author.id == item.author.id
is Message -> Message(self, item, wasPreviousMessageSameSender)
is RoomEvent.Reply -> Reply(self, item, wasPreviousMessageSameSender)
is RoomEvent.Image -> Image(self, item, wasPreviousMessageSameSender)
}
}
}
}
@Composable
private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPreviousMessageSameSender: Boolean) {
when (message.author.id == self) {
true -> {
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) {
Box(modifier = Modifier.fillParentMaxWidth(0.85f), contentAlignment = Alignment.TopEnd) {
Bubble(
message = message,
isNotSelf = false,
wasPreviousMessageSameSender = wasPreviousMessageSameSender
) {
Text(message.imageMeta.url)
androidx.compose.foundation.Image(
painter = rememberImagePainter(
data = message.imageMeta.url,
),
contentDescription = null,
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
)
}
Message(self, item, wasPreviousMessageSameSender)
}
is RoomEvent.Reply -> {
val wasPreviousMessageSameSender = when (val previousEvent = if (index != 0) state.events[index - 1] else null) {
null -> false
is Message -> previousEvent.author.id == item.message.author.id
is RoomEvent.Reply -> previousEvent.message.author.id == item.message.author.id
}
Reply(self, item, wasPreviousMessageSameSender)
}
}
false -> {
Box(modifier = Modifier.fillParentMaxWidth(0.95f), contentAlignment = Alignment.TopStart) {
Bubble(
message = message,
isNotSelf = true,
wasPreviousMessageSameSender = wasPreviousMessageSameSender
) {
Text(message.imageMeta.url)
androidx.compose.foundation.Image(
painter = rememberImagePainter(
data = message.imageMeta.url,
builder = {
}
),
contentDescription = null,
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
)
}
}
}
}
}
@Composable
private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) {
when (message.author.id == self) {
@ -243,7 +286,7 @@ private val othersBackgroundShape = RoundedCornerShape(0.dp, 12.dp, 12.dp, 12.dp
@Composable
private fun Bubble(
message: Message,
message: RoomEvent,
isNotSelf: Boolean,
wasPreviousMessageSameSender: Boolean,
content: @Composable () -> Unit
@ -346,13 +389,17 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN
maxLines = 1,
color = MaterialTheme.colors.onPrimary
)
Text(
text = reply.replyingTo.content,
color = MaterialTheme.colors.onPrimary,
fontSize = 15.sp,
modifier = Modifier.wrapContentSize(),
textAlign = TextAlign.Start,
)
when (val replyingTo = reply.replyingTo) {
is Message -> {
Text(
text = replyingTo.content,
color = MaterialTheme.colors.onPrimary,
fontSize = 15.sp,
modifier = Modifier.wrapContentSize(),
textAlign = TextAlign.Start,
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
@ -365,13 +412,17 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN
color = MaterialTheme.colors.onPrimary
)
}
Text(
text = reply.message.content,
color = MaterialTheme.colors.onPrimary,
fontSize = 15.sp,
modifier = Modifier.wrapContentSize(),
textAlign = TextAlign.Start,
)
when (val message = reply.message) {
is Message -> {
Text(
text = message.content,
color = MaterialTheme.colors.onPrimary,
fontSize = 15.sp,
modifier = Modifier.wrapContentSize(),
textAlign = TextAlign.Start,
)
}
}
Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
@ -391,7 +442,7 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN
@Composable
private fun RowScope.SendStatus(message: Message) {
private fun RowScope.SendStatus(message: RoomEvent) {
when (val meta = message.meta) {
MessageMeta.FromServer -> {
// last message is self

View File

@ -4,7 +4,6 @@ import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.matrix.common.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
@ -23,6 +22,8 @@ sealed class RoomEvent {
abstract val eventId: EventId
abstract val utcTimestamp: Long
abstract val author: RoomMember
abstract val meta: MessageMeta
@Serializable
@SerialName("message")
@ -30,8 +31,8 @@ sealed class RoomEvent {
@SerialName("event_id") override val eventId: EventId,
@SerialName("timestamp") override val utcTimestamp: Long,
@SerialName("content") val content: String,
@SerialName("author") val author: RoomMember,
@SerialName("meta") val meta: MessageMeta,
@SerialName("author") override val author: RoomMember,
@SerialName("meta") override val meta: MessageMeta,
@SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null,
@SerialName("edited") val edited: Boolean = false,
) : RoomEvent() {
@ -44,7 +45,6 @@ sealed class RoomEvent {
@SerialName("session_id") val sessionId: SessionId,
)
@Transient
val time: String by unsafeLazy {
val instant = Instant.ofEpochMilli(utcTimestamp)
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
@ -54,22 +54,48 @@ sealed class RoomEvent {
@Serializable
@SerialName("reply")
data class Reply(
@SerialName("message") val message: Message,
@SerialName("in_reply_to") val replyingTo: Message,
@SerialName("message") val message: RoomEvent,
@SerialName("in_reply_to") 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
@Transient
val time: String by unsafeLazy {
val instant = Instant.ofEpochMilli(utcTimestamp)
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
}
}
@Serializable
@SerialName("image")
data class Image(
@SerialName("event_id") override val eventId: EventId,
@SerialName("timestamp") override val utcTimestamp: Long,
@SerialName("image_meta") val imageMeta: ImageMeta,
@SerialName("author") override val author: RoomMember,
@SerialName("meta") override val meta: MessageMeta,
@SerialName("encrypted_content") val encryptedContent: Message.MegOlmV1? = null,
@SerialName("edited") val edited: Boolean = false,
) : RoomEvent() {
val time: String by unsafeLazy {
val instant = Instant.ofEpochMilli(utcTimestamp)
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
}
@Serializable
data class ImageMeta(
@SerialName("width") val width: Int,
@SerialName("height") val height: Int,
@SerialName("url") val url: String,
)
}
}
@Serializable

View File

@ -35,7 +35,7 @@ internal class DefaultSyncService(
json: Json,
oneTimeKeyProducer: MaybeCreateMoreKeys,
scope: CoroutineScope,
credentialsStore: CredentialsStore,
private val credentialsStore: CredentialsStore,
roomMembersService: RoomMembersService,
logger: MatrixLogger,
errorTracker: ErrorTracker,
@ -57,7 +57,7 @@ internal class DefaultSyncService(
roomMembersService,
roomDataSource,
TimelineEventsProcessor(
RoomEventCreator(roomMembersService, logger, errorTracker),
RoomEventCreator(roomMembersService, errorTracker, RoomEventFactory(roomMembersService)),
roomEventsDecrypter,
eventDecrypter,
EventLookupUseCase(roomStore)
@ -69,6 +69,7 @@ internal class DefaultSyncService(
roomRefresher,
roomDataSource,
logger,
errorTracker,
coroutineDispatchers,
)
SyncUseCase(
@ -114,7 +115,7 @@ internal class DefaultSyncService(
coroutineDispatchers.withIoContext {
roomIds.map {
async {
roomRefresher.refreshRoomContent(it)?.also {
roomRefresher.refreshRoomContent(it, credentialsStore.credentials()!!)?.also {
overviewStore.persist(listOf(it.roomOverview))
}
}

View File

@ -445,13 +445,44 @@ internal sealed class ApiTimelineEvent {
@SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null
) : ApiTimelineEvent() {
@Serializable
internal data class Content(
@SerialName("body") val body: String? = null,
@SerialName("formatted_body") val formattedBody: String? = null,
@SerialName("msgtype") val type: String? = null,
@SerialName("m.relates_to") val relation: Relation? = null,
)
@Serializable(with = ApiTimelineMessageContentDeserializer::class)
internal sealed interface Content {
val relation: Relation?
@Serializable
data class Text(
@SerialName("body") val body: String? = null,
@SerialName("formatted_body") val formattedBody: String? = null,
@SerialName("m.relates_to") override val relation: Relation? = null,
@SerialName("msgtype") val messageType: String = "m.text",
) : Content
@Serializable
data class Image(
@SerialName("file") val file: File,
@SerialName("info") val info: Info,
@SerialName("m.relates_to") override val relation: Relation? = null,
@SerialName("msgtype") val messageType: String = "m.image",
) : Content {
@Serializable
data class File(
@SerialName("url") val url: MxUrl,
)
@Serializable
internal data class Info(
@SerialName("h") val height: Int,
@SerialName("w") val width: Int,
)
}
@Serializable
object Ignored : Content {
override val relation: Relation? = null
}
}
@Serializable
data class Relation(
@ -512,3 +543,31 @@ internal object EncryptedContentDeserializer : KSerializer<ApiEncryptedContent>
override fun serialize(encoder: Encoder, value: ApiEncryptedContent) = TODO("Not yet implemented")
}
internal object ApiTimelineMessageContentDeserializer : KSerializer<ApiTimelineEvent.TimelineMessage.Content> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("messageContent")
override fun deserialize(decoder: Decoder): ApiTimelineEvent.TimelineMessage.Content {
require(decoder is JsonDecoder)
val element = decoder.decodeJsonElement()
return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) {
"m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder)
"m.image" -> when (element.jsonObject["file"]) {
null -> ApiTimelineEvent.TimelineMessage.Content.Ignored
else -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
}
else -> {
println(element)
ApiTimelineEvent.TimelineMessage.Content.Ignored
}
}
}
override fun serialize(encoder: Encoder, value: ApiTimelineEvent.TimelineMessage.Content) = when (value) {
ApiTimelineEvent.TimelineMessage.Content.Ignored -> {}
is ApiTimelineEvent.TimelineMessage.Content.Image -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().serialize(encoder, value)
is ApiTimelineEvent.TimelineMessage.Content.Text -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().serialize(encoder, value)
}
}

View File

@ -2,6 +2,7 @@ package app.dapk.st.matrix.sync.internal.room
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
import app.dapk.st.matrix.sync.internal.request.DecryptedContent
import kotlinx.serialization.json.Json
@ -11,13 +12,37 @@ internal class RoomEventsDecrypter(
private val logger: MatrixLogger,
) {
suspend fun decryptRoomEvents(events: List<RoomEvent>) = events.map { event ->
when (event) {
is RoomEvent.Message -> event.decrypt()
is RoomEvent.Reply -> RoomEvent.Reply(
message = event.message.decrypt(),
replyingTo = event.replyingTo.decrypt(),
)
suspend fun decryptRoomEvents(userCredentials: UserCredentials, events: List<RoomEvent>) = events.map { event ->
decryptEvent(event, userCredentials)
}
private suspend fun decryptEvent(event: RoomEvent, userCredentials: UserCredentials): RoomEvent = when (event) {
is RoomEvent.Message -> event.decrypt()
is RoomEvent.Reply -> RoomEvent.Reply(
message = decryptEvent(event.message, userCredentials),
replyingTo = decryptEvent(event.replyingTo, userCredentials),
)
is RoomEvent.Image -> event.decrypt(userCredentials)
}
private suspend fun RoomEvent.Image.decrypt(userCredentials: UserCredentials) = when (this.encryptedContent) {
null -> this
else -> when (val result = messageDecrypter.decrypt(this.encryptedContent.toModel())) {
is DecryptionResult.Failed -> this.also { logger.crypto("Failed to decrypt ${it.eventId}") }
is DecryptionResult.Success -> when (val model = result.payload.toModel()) {
DecryptedContent.Ignored -> this
is DecryptedContent.TimelineText -> {
val content = model.content as ApiTimelineEvent.TimelineMessage.Content.Image
this.copy(
imageMeta = RoomEvent.Image.ImageMeta(
width = content.info.width,
height = content.info.height,
url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: ""
),
encryptedContent = null,
)
}
}
}
}
@ -35,10 +60,9 @@ internal class RoomEventsDecrypter(
private fun JsonString.toModel() = json.decodeFromString(DecryptedContent.serializer(), this.value)
private fun RoomEvent.Message.copyWithDecryptedContent(decryptedContent: DecryptedContent.TimelineText) = this.copy(
content = decryptedContent.content.body ?: "",
content = (decryptedContent.content as ApiTimelineEvent.TimelineMessage.Content.Text).body ?: "",
encryptedContent = null
)
}
private fun RoomEvent.Message.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1(

View File

@ -39,7 +39,11 @@ internal class SyncEventDecrypter(
is DecryptedContent.TimelineText -> ApiTimelineEvent.TimelineMessage(
event.eventId,
event.senderId,
it.content.copy(relation = relation),
when (it.content) {
is ApiTimelineEvent.TimelineMessage.Content.Image -> it.content.copy(relation = relation)
is ApiTimelineEvent.TimelineMessage.Content.Text -> it.content.copy(relation = relation)
ApiTimelineEvent.TimelineMessage.Content.Ignored -> it.content
},
event.utcTimestamp,
).also { logger.matrixLog("decrypted to timeline text: $it") }
DecryptedContent.Ignored -> event

View File

@ -4,9 +4,8 @@ import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.ifOrNull
import app.dapk.st.core.extensions.nullAndTrack
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.MatrixLogger
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.matrixLog
import app.dapk.st.matrix.common.UserCredentials
import app.dapk.st.matrix.sync.MessageMeta
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomMembersService
@ -18,8 +17,8 @@ private typealias Lookup = suspend (EventId) -> LookupResult
internal class RoomEventCreator(
private val roomMembersService: RoomMembersService,
private val logger: MatrixLogger,
private val errorTracker: ErrorTracker,
private val roomEventFactory: RoomEventFactory,
) {
suspend fun ApiTimelineEvent.Encrypted.toRoomEvent(roomId: RoomId): RoomEvent? {
@ -44,82 +43,122 @@ internal class RoomEventCreator(
}
}
suspend fun ApiTimelineEvent.TimelineMessage.toRoomEvent(roomId: RoomId, lookup: Lookup): RoomEvent? {
suspend fun ApiTimelineEvent.TimelineMessage.toRoomEvent(userCredentials: UserCredentials, roomId: RoomId, lookup: Lookup): RoomEvent? {
return TimelineEventMapper(userCredentials, roomId, roomEventFactory).mapToRoomEvent(this, lookup)
}
}
internal class TimelineEventMapper(
private val userCredentials: UserCredentials,
private val roomId: RoomId,
private val roomEventFactory: RoomEventFactory,
) {
suspend fun mapToRoomEvent(event: ApiTimelineEvent.TimelineMessage, lookup: Lookup): RoomEvent? {
return when {
this.isEdit() -> handleEdit(roomId, this.content.relation!!.eventId!!, lookup)
this.isReply() -> handleReply(roomId, lookup)
else -> this.toMessage(roomId)
event.content == ApiTimelineEvent.TimelineMessage.Content.Ignored -> null
event.isEdit() -> event.handleEdit(editedEventId = event.content.relation!!.eventId!!, lookup)
event.isReply() -> event.handleReply(replyToId = event.content.relation!!.inReplyTo!!.eventId, lookup)
else -> roomEventFactory.mapToRoomEvent(event)
}
}
private suspend fun ApiTimelineEvent.TimelineMessage.handleEdit(roomId: RoomId, editedEventId: EventId, lookup: Lookup): RoomEvent? {
return lookup(editedEventId).fold(
onApiTimelineEvent = {
ifOrNull(this.utcTimestamp > it.utcTimestamp) {
it.toMessage(
roomId,
utcTimestamp = this.utcTimestamp,
content = this.content.body?.removePrefix(" * ")?.trim() ?: "redacted",
edited = true,
)
}
},
onRoomEvent = {
ifOrNull(this.utcTimestamp > it.utcTimestamp) {
when (it) {
is RoomEvent.Message -> it.edited(this)
is RoomEvent.Reply -> it.copy(message = it.message.edited(this))
}
}
},
onEmpty = { this.toMessage(roomId, edited = true) }
)
}
private fun RoomEvent.Message.edited(edit: ApiTimelineEvent.TimelineMessage) = this.copy(
content = edit.content.body?.removePrefix(" * ")?.trim() ?: "redacted",
utcTimestamp = edit.utcTimestamp,
edited = true,
)
private suspend fun ApiTimelineEvent.TimelineMessage.handleReply(roomId: RoomId, lookup: Lookup): RoomEvent {
val replyTo = this.content.relation!!.inReplyTo!!
val relationEvent = lookup(replyTo.eventId).fold(
onApiTimelineEvent = { it.toMessage(roomId) },
private suspend fun ApiTimelineEvent.TimelineMessage.handleReply(replyToId: EventId, lookup: Lookup): RoomEvent {
val relationEvent = lookup(replyToId).fold(
onApiTimelineEvent = { it.toTextMessage() },
onRoomEvent = { it },
onEmpty = { null }
)
logger.matrixLog("found relation: $relationEvent")
return when (relationEvent) {
null -> this.toMessage(roomId)
null -> when (this.content) {
is ApiTimelineEvent.TimelineMessage.Content.Image -> this.toImageMessage()
is ApiTimelineEvent.TimelineMessage.Content.Text -> this.toFallbackTextMessage()
ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException()
}
else -> {
RoomEvent.Reply(
message = this.toMessage(roomId, content = this.content.formattedBody?.stripTags() ?: "redacted"),
message = roomEventFactory.mapToRoomEvent(this),
replyingTo = when (relationEvent) {
is RoomEvent.Message -> relationEvent
is RoomEvent.Reply -> relationEvent.message
is RoomEvent.Image -> relationEvent
}
)
}
}
}
private suspend fun ApiTimelineEvent.TimelineMessage.toMessage(
roomId: RoomId,
content: String = this.content.body ?: "redacted",
private suspend fun ApiTimelineEvent.TimelineMessage.toFallbackTextMessage() = this.toTextMessage(content = this.asTextContent().body ?: "redacted")
private suspend fun ApiTimelineEvent.TimelineMessage.handleEdit(editedEventId: EventId, lookup: Lookup): RoomEvent? {
return lookup(editedEventId).fold(
onApiTimelineEvent = { editApiEvent(original = it, incomingEdit = this) },
onRoomEvent = { editRoomEvent(original = it, incomingEdit = this) },
onEmpty = { this.toTextMessage(edited = true) }
)
}
private fun editRoomEvent(original: RoomEvent, incomingEdit: ApiTimelineEvent.TimelineMessage): RoomEvent? {
return ifOrNull(incomingEdit.utcTimestamp > original.utcTimestamp) {
when (original) {
is RoomEvent.Message -> original.edited(incomingEdit)
is RoomEvent.Reply -> original.copy(
message = when (original.message) {
is RoomEvent.Image -> original.message
is RoomEvent.Message -> original.message.edited(incomingEdit)
is RoomEvent.Reply -> original.message
}
)
is RoomEvent.Image -> {
// can't edit images
null
}
}
}
}
private suspend fun editApiEvent(original: ApiTimelineEvent.TimelineMessage, incomingEdit: ApiTimelineEvent.TimelineMessage): RoomEvent? {
return ifOrNull(incomingEdit.utcTimestamp > original.utcTimestamp) {
when (original.content) {
is ApiTimelineEvent.TimelineMessage.Content.Image -> original.toImageMessage(
utcTimestamp = incomingEdit.utcTimestamp,
edited = true,
)
is ApiTimelineEvent.TimelineMessage.Content.Text -> original.toTextMessage(
utcTimestamp = incomingEdit.utcTimestamp,
content = incomingEdit.asTextContent().body?.removePrefix(" * ")?.trim() ?: "redacted",
edited = true,
)
ApiTimelineEvent.TimelineMessage.Content.Ignored -> null
}
}
}
private fun RoomEvent.Message.edited(edit: ApiTimelineEvent.TimelineMessage) = this.copy(
content = edit.asTextContent().body?.removePrefix(" * ")?.trim() ?: "redacted",
utcTimestamp = edit.utcTimestamp,
edited = true,
)
private suspend fun RoomEventFactory.mapToRoomEvent(source: ApiTimelineEvent.TimelineMessage): RoomEvent {
return when (source.content) {
is ApiTimelineEvent.TimelineMessage.Content.Image -> source.toImageMessage(userCredentials, roomId)
is ApiTimelineEvent.TimelineMessage.Content.Text -> source.toTextMessage(roomId)
ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException()
}
}
private suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage(
content: String = this.asTextContent().formattedBody?.stripTags() ?: this.asTextContent().body ?: "redacted",
edited: Boolean = false,
utcTimestamp: Long = this.utcTimestamp,
) = RoomEvent.Message(
eventId = this.id,
content = content,
author = roomMembersService.find(roomId, this.senderId)!!,
utcTimestamp = utcTimestamp,
meta = MessageMeta.FromServer,
edited = edited,
)
) = with(roomEventFactory) { toTextMessage(roomId, content, edited, utcTimestamp) }
private suspend fun ApiTimelineEvent.TimelineMessage.toImageMessage(
edited: Boolean = false,
utcTimestamp: Long = this.utcTimestamp,
) = with(roomEventFactory) { toImageMessage(userCredentials, roomId, edited, utcTimestamp) }
}
@ -128,5 +167,6 @@ private fun String.stripTags() = this.substring(this.indexOf("</mx-reply>") + "<
.replace("<em>", "")
.replace("</em>", "")
private fun ApiTimelineEvent.TimelineMessage.isEdit() = this.content.relation?.relationType == "m.replace" && this.content.relation.eventId != null
private fun ApiTimelineEvent.TimelineMessage.isReply() = this.content.relation?.inReplyTo != null
private fun ApiTimelineEvent.TimelineMessage.isEdit() = this.content.relation?.relationType == "m.replace" && this.content.relation?.eventId != null
private fun ApiTimelineEvent.TimelineMessage.isReply() = this.content.relation?.inReplyTo != null
private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text

View File

@ -0,0 +1,60 @@
package app.dapk.st.matrix.sync.internal.sync
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserCredentials
import app.dapk.st.matrix.common.convertMxUrToUrl
import app.dapk.st.matrix.sync.MessageMeta
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomMembersService
import app.dapk.st.matrix.sync.find
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
internal class RoomEventFactory(
private val roomMembersService: RoomMembersService
) {
suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage(
roomId: RoomId,
content: String = this.asTextContent().formattedBody?.stripTags() ?: this.asTextContent().body ?: "redacted",
edited: Boolean = false,
utcTimestamp: Long = this.utcTimestamp,
) = RoomEvent.Message(
eventId = this.id,
content = content,
author = roomMembersService.find(roomId, this.senderId)!!,
utcTimestamp = utcTimestamp,
meta = MessageMeta.FromServer,
edited = edited,
)
suspend fun ApiTimelineEvent.TimelineMessage.toImageMessage(
userCredentials: UserCredentials,
roomId: RoomId,
edited: Boolean = false,
utcTimestamp: Long = this.utcTimestamp,
imageMeta: RoomEvent.Image.ImageMeta = this.readImageMeta(userCredentials)
) = RoomEvent.Image(
eventId = this.id,
imageMeta = imageMeta,
author = roomMembersService.find(roomId, this.senderId)!!,
utcTimestamp = utcTimestamp,
meta = MessageMeta.FromServer,
edited = edited,
)
private fun ApiTimelineEvent.TimelineMessage.readImageMeta(userCredentials: UserCredentials): RoomEvent.Image.ImageMeta {
val content = this.content as ApiTimelineEvent.TimelineMessage.Content.Image
return RoomEvent.Image.ImageMeta(
content.info.width,
content.info.height,
content.file?.url?.convertMxUrToUrl(userCredentials.homeServer)
)
}
}
private fun String.stripTags() = this.substring(this.indexOf("</mx-reply>") + "</mx-reply>".length)
.trim()
.replace("<em>", "")
.replace("</em>", "")
private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text

View File

@ -1,9 +1,6 @@
package app.dapk.st.matrix.sync.internal.sync
import app.dapk.st.matrix.common.MatrixLogTag
import app.dapk.st.matrix.common.MatrixLogger
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.matrixLog
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
@ -14,13 +11,13 @@ internal class RoomRefresher(
private val logger: MatrixLogger
) {
suspend fun refreshRoomContent(roomId: RoomId): RoomState? {
suspend fun refreshRoomContent(roomId: RoomId, userCredentials: UserCredentials): RoomState? {
logger.matrixLog(MatrixLogTag.SYNC, "reducing side effect: $roomId")
return when (val previousState = roomDataSource.read(roomId)) {
null -> null.also { logger.matrixLog(MatrixLogTag.SYNC, "no previous state to update") }
else -> {
logger.matrixLog(MatrixLogTag.SYNC, "previous state updated")
val decryptedEvents = previousState.events.decryptEvents()
val decryptedEvents = previousState.events.decryptEvents(userCredentials)
val lastMessage = decryptedEvents.sortedByDescending { it.utcTimestamp }.findLastMessage()
previousState.copy(events = decryptedEvents, roomOverview = previousState.roomOverview.copy(lastMessage = lastMessage)).also {
@ -30,6 +27,6 @@ internal class RoomRefresher(
}
}
private suspend fun List<RoomEvent>.decryptEvents() = roomEventsDecrypter.decryptRoomEvents(this)
private suspend fun List<RoomEvent>.decryptEvents(userCredentials: UserCredentials) = roomEventsDecrypter.decryptRoomEvents(userCredentials, this)
}

View File

@ -1,6 +1,7 @@
package app.dapk.st.matrix.sync.internal.sync
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.withIoContextAsync
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.common.MatrixLogTag.SYNC
@ -16,6 +17,7 @@ internal class SyncReducer(
private val roomRefresher: RoomRefresher,
private val roomDataSource: RoomDataSource,
private val logger: MatrixLogger,
private val errorTracker: ErrorTracker,
private val coroutineDispatchers: CoroutineDispatchers,
) {
@ -48,14 +50,14 @@ internal class SyncReducer(
isInitialSync = isInitialSync
)
}
.onFailure { logger.matrixLog(SYNC, "failed to reduce: $roomId, skipping") }
.onFailure { errorTracker.track(it, "failed to reduce: $roomId, skipping") }
.getOrNull()
}
} ?: emptyList()
val roomsWithSideEffects = sideEffects.roomsToRefresh(alreadyHandledRooms = apiUpdatedRooms?.keys ?: emptySet()).map { roomId ->
coroutineDispatchers.withIoContextAsync {
roomRefresher.refreshRoomContent(roomId)
roomRefresher.refreshRoomContent(roomId, userCredentials)
}
}

View File

@ -1,5 +1,6 @@
package app.dapk.st.matrix.sync.internal.sync
import app.dapk.st.matrix.common.UserCredentials
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
@ -22,13 +23,13 @@ internal class TimelineEventsProcessor(
private suspend fun processNewEvents(roomToProcess: RoomToProcess, previousEvents: List<RoomEvent>): List<RoomEvent> {
val decryptedTimeline = roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.decryptEvents()
val decryptedPreviousEvents = previousEvents.decryptEvents()
val decryptedPreviousEvents = previousEvents.decryptEvents(roomToProcess.userCredentials)
val newEvents = with(roomEventCreator) {
decryptedTimeline.value.mapNotNull { event ->
val roomEvent = when (event) {
is ApiTimelineEvent.Encrypted -> event.toRoomEvent(roomToProcess.roomId)
is ApiTimelineEvent.TimelineMessage -> event.toRoomEvent(roomToProcess.roomId) { eventId ->
is ApiTimelineEvent.TimelineMessage -> event.toRoomEvent(roomToProcess.userCredentials, roomToProcess.roomId) { eventId ->
eventLookupUseCase.lookup(eventId, decryptedTimeline, decryptedPreviousEvents)
}
is ApiTimelineEvent.Encryption -> null
@ -46,7 +47,8 @@ internal class TimelineEventsProcessor(
}
private suspend fun List<ApiTimelineEvent>.decryptEvents() = DecryptedTimeline(eventDecrypter.decryptTimelineEvents(this))
private suspend fun List<RoomEvent>.decryptEvents() = DecryptedRoomEvents(roomEventsDecrypter.decryptRoomEvents(this))
private suspend fun List<RoomEvent>.decryptEvents(userCredentials: UserCredentials) =
DecryptedRoomEvents(roomEventsDecrypter.decryptRoomEvents(userCredentials, this))
}

View File

@ -42,6 +42,7 @@ internal class UnreadEventsUseCase(
when (it) {
is RoomEvent.Message -> it.author.id == selfId
is RoomEvent.Reply -> it.message.author.id == selfId
is RoomEvent.Image -> it.author.id == selfId
}
}.map { it.eventId }
roomStore.insertUnread(overview.roomId, eventsFromOthers)

View File

@ -21,6 +21,7 @@ private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent(
replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event"))
)
private val A_DECRYPTED_CONTENT = DecryptedContent.TimelineText(aTimelineTextEventContent(body = A_DECRYPTED_MESSAGE_CONTENT))
private val A_USER_CREDENTIALS = aUserCredentials()
class RoomEventsDecrypterTest {
@ -35,7 +36,7 @@ class RoomEventsDecrypterTest {
@Test
fun `given clear message event, when decrypting, then does nothing`() = runTest {
val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null)
val result = roomEventsDecrypter.decryptRoomEvents(listOf(aClearMessageEvent))
val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent))
result shouldBeEqualTo listOf(aClearMessageEvent)
}
@ -44,7 +45,7 @@ class RoomEventsDecrypterTest {
fun `given encrypted message event, when decrypting, then applies decrypted body and removes encrypted content`() = runTest {
givenEncryptedMessage(AN_ENCRYPTED_ROOM_MESSAGE, decryptsTo = A_DECRYPTED_CONTENT)
val result = roomEventsDecrypter.decryptRoomEvents(listOf(AN_ENCRYPTED_ROOM_MESSAGE))
val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(AN_ENCRYPTED_ROOM_MESSAGE))
result shouldBeEqualTo listOf(AN_ENCRYPTED_ROOM_MESSAGE.copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null))
}
@ -53,12 +54,12 @@ class RoomEventsDecrypterTest {
fun `given encrypted reply event, when decrypting, then decrypts message and replyTo`() = runTest {
givenEncryptedReply(AN_ENCRYPTED_ROOM_REPLY, decryptsTo = A_DECRYPTED_CONTENT)
val result = roomEventsDecrypter.decryptRoomEvents(listOf(AN_ENCRYPTED_ROOM_REPLY))
val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(AN_ENCRYPTED_ROOM_REPLY))
result shouldBeEqualTo listOf(
AN_ENCRYPTED_ROOM_REPLY.copy(
message = AN_ENCRYPTED_ROOM_REPLY.message.copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null),
replyingTo = AN_ENCRYPTED_ROOM_REPLY.replyingTo.copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null),
message = (AN_ENCRYPTED_ROOM_REPLY.message as RoomEvent.Message).copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null),
replyingTo = (AN_ENCRYPTED_ROOM_REPLY.replyingTo as RoomEvent.Message).copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null),
)
)
}
@ -66,12 +67,12 @@ class RoomEventsDecrypterTest {
private fun givenEncryptedMessage(roomMessage: RoomEvent.Message, decryptsTo: DecryptedContent) {
val model = roomMessage.encryptedContent!!.toModel()
fakeMessageDecrypter.givenDecrypt(model)
.returns(aDecryptionSuccessResult(payload = JsonString(Json.encodeToString(DecryptedContent.serializer(), decryptsTo))))
.returns(aDecryptionSuccessResult(payload = JsonString(Json { encodeDefaults = true }.encodeToString(DecryptedContent.serializer(), decryptsTo))))
}
private fun givenEncryptedReply(roomReply: RoomEvent.Reply, decryptsTo: DecryptedContent) {
givenEncryptedMessage(roomReply.message, decryptsTo)
givenEncryptedMessage(roomReply.replyingTo, decryptsTo)
givenEncryptedMessage(roomReply.message as RoomEvent.Message, decryptsTo)
givenEncryptedMessage(roomReply.replyingTo as RoomEvent.Message, decryptsTo)
}
}

View File

@ -26,12 +26,13 @@ private val A_TEXT_EVENT_WITHOUT_CONTENT = anApiTimelineTextEvent(
senderId = A_SENDER.id,
content = aTimelineTextEventContent(body = null)
)
private val A_USER_CREDENTIALS = aUserCredentials()
internal class RoomEventCreatorTest {
private val fakeRoomMembersService = FakeRoomMembersService()
private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeMatrixLogger(), FakeErrorTracker())
private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeErrorTracker(), RoomEventFactory(fakeRoomMembersService))
@Test
fun `given Megolm encrypted event then maps to encrypted room message`() = runTest {
@ -71,7 +72,7 @@ internal class RoomEventCreatorTest {
fun `given text event then maps to room message`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_ROOM_ID, EMPTY_LOOKUP) }
val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
result shouldBeEqualTo aRoomMessageEvent(
eventId = A_TEXT_EVENT.id,
@ -85,7 +86,7 @@ internal class RoomEventCreatorTest {
fun `given text event without body then maps to redacted room message`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_ROOM_ID, EMPTY_LOOKUP) }
val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
result shouldBeEqualTo aRoomMessageEvent(
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
@ -100,12 +101,12 @@ internal class RoomEventCreatorTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val editEvent = anApiTimelineTextEvent().toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
val result = with(roomEventCreator) { editEvent.toRoomEvent(A_ROOM_ID, EMPTY_LOOKUP) }
val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
result shouldBeEqualTo aRoomMessageEvent(
eventId = editEvent.id,
utcTimestamp = editEvent.utcTimestamp,
content = editEvent.content.body!!,
content = editEvent.asTextContent().body!!,
author = A_SENDER,
edited = true
)
@ -118,7 +119,7 @@ internal class RoomEventCreatorTest {
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) }
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomMessageEvent(
eventId = originalMessage.id,
@ -136,7 +137,7 @@ internal class RoomEventCreatorTest {
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) }
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomMessageEvent(
eventId = originalMessage.eventId,
@ -151,10 +152,10 @@ internal class RoomEventCreatorTest {
fun `given edited event which relates to a room reply event then only updates message`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val originalMessage = aRoomReplyMessageEvent(message = aRoomMessageEvent())
val editedMessage = originalMessage.message.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) }
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage.replyingTo,
@ -174,7 +175,7 @@ internal class RoomEventCreatorTest {
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) }
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo null
}
@ -185,22 +186,23 @@ internal class RoomEventCreatorTest {
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_ROOM_ID, lookup) }
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo null
}
@Test
fun `given reply event with no relation then maps to new room message`() = runTest {
fun `given reply event with no relation then maps to new room message using the full body`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val replyEvent = anApiTimelineTextEvent().toReplyEvent(messageContent = A_TEXT_EVENT_MESSAGE)
val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_ROOM_ID, EMPTY_LOOKUP) }
println(replyEvent.content)
val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
result shouldBeEqualTo aRoomMessageEvent(
eventId = replyEvent.id,
utcTimestamp = replyEvent.utcTimestamp,
content = "${replyEvent.content.body}",
content = replyEvent.asTextContent().body!!,
author = A_SENDER,
)
}
@ -212,13 +214,13 @@ internal class RoomEventCreatorTest {
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_ROOM_ID, lookup) }
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = aRoomMessageEvent(
eventId = originalMessage.id,
utcTimestamp = originalMessage.utcTimestamp,
content = originalMessage.content.body!!,
content = originalMessage.asTextContent().body!!,
author = A_SENDER,
),
message = aRoomMessageEvent(
@ -237,7 +239,7 @@ internal class RoomEventCreatorTest {
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_ROOM_ID, lookup) }
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage,
@ -254,10 +256,10 @@ internal class RoomEventCreatorTest {
fun `given reply event which relates to another room reply event then maps to reply with the reply's message`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val originalMessage = aRoomReplyMessageEvent()
val replyMessage = originalMessage.message.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
val replyMessage = (originalMessage.message as RoomEvent.Message).toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_ROOM_ID, lookup) }
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage.message,
@ -326,4 +328,6 @@ private fun ApiEncryptedContent.toMegolm(): RoomEvent.Message.MegOlmV1 {
private class FakeLookup(private val result: LookupResult) : suspend (EventId) -> LookupResult {
override suspend fun invoke(p1: EventId) = result
}
}
private fun ApiTimelineEvent.TimelineMessage.asTextContent() = this.content as ApiTimelineEvent.TimelineMessage.Content.Text

View File

@ -22,6 +22,7 @@ private object ARoom {
val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT)
val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS)
}
private val A_USER_CREDENTIALS = aUserCredentials()
internal class RoomRefresherTest {
@ -38,7 +39,7 @@ internal class RoomRefresherTest {
fun `given no existing room when refreshing then does nothing`() = runTest {
fakeRoomDataSource.givenNoCachedRoom(A_ROOM_ID)
val result = roomRefresher.refreshRoomContent(aRoomId())
val result = roomRefresher.refreshRoomContent(aRoomId(), A_USER_CREDENTIALS)
result shouldBeEqualTo null
fakeRoomDataSource.verifyNoChanges()
@ -48,9 +49,9 @@ internal class RoomRefresherTest {
fun `given existing room when refreshing then processes existing state`() = runTest {
fakeRoomDataSource.expect { it.instance.persist(RoomId(any()), any(), any()) }
fakeRoomDataSource.givenRoom(A_ROOM_ID, ARoom.PREVIOUS_STATE)
fakeRoomEventsDecrypter.givenDecrypts(ARoom.PREVIOUS_STATE.events, ARoom.DECRYPTED_EVENTS)
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, ARoom.PREVIOUS_STATE.events, ARoom.DECRYPTED_EVENTS)
val result = roomRefresher.refreshRoomContent(aRoomId())
val result = roomRefresher.refreshRoomContent(aRoomId(), A_USER_CREDENTIALS)
fakeRoomDataSource.verifyRoomUpdated(ARoom.PREVIOUS_STATE, ARoom.NEW_STATE)
result shouldBeEqualTo ARoom.NEW_STATE

View File

@ -22,6 +22,7 @@ private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent()
private val A_MESSAGE_ROOM_EVENT = aRoomMessageEvent(anEventId("a-message"))
private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message"))
private val A_LOOKUP_EVENT_ID = anEventId("lookup-id")
private val A_USER_CREDENTIALS = aUserCredentials()
class TimelineEventsProcessorTest {
@ -41,7 +42,7 @@ class TimelineEventsProcessorTest {
fun `given a room with no events then returns empty`() = runTest {
val previousEvents = emptyList<RoomEvent>()
val roomToProcess = aRoomToProcess()
fakeRoomEventsDecrypter.givenDecrypts(previousEvents)
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
fakeSyncEventDecrypter.givenDecrypts(roomToProcess.apiSyncRoom.timeline.apiTimelineEvents)
val result = timelineEventsProcessor.process(roomToProcess, previousEvents)
@ -54,11 +55,18 @@ class TimelineEventsProcessorTest {
val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event")))
val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT)
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
fakeRoomEventsDecrypter.givenDecrypts(previousEvents)
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents)
fakeEventLookup.givenLookup(A_LOOKUP_EVENT_ID, DecryptedTimeline(newTimelineEvents), DecryptedRoomEvents(previousEvents), ANY_LOOKUP_RESULT)
fakeRoomEventCreator.givenCreates(A_ROOM_ID, AN_ENCRYPTED_TIMELINE_EVENT, AN_ENCRYPTED_ROOM_EVENT)
fakeRoomEventCreator.givenCreatesUsingLookup(A_ROOM_ID, A_LOOKUP_EVENT_ID, A_TEXT_TIMELINE_EVENT, A_MESSAGE_ROOM_EVENT, ANY_LOOKUP_RESULT)
fakeRoomEventCreator.givenCreatesUsingLookup(
A_USER_CREDENTIALS,
A_ROOM_ID,
A_LOOKUP_EVENT_ID,
A_TEXT_TIMELINE_EVENT,
A_MESSAGE_ROOM_EVENT,
ANY_LOOKUP_RESULT
)
val result = timelineEventsProcessor.process(roomToProcess, previousEvents)
@ -79,7 +87,7 @@ class TimelineEventsProcessorTest {
anIgnoredApiTimelineEvent()
)
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
fakeRoomEventsDecrypter.givenDecrypts(previousEvents)
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents)
val result = timelineEventsProcessor.process(roomToProcess, previousEvents)

View File

@ -2,6 +2,7 @@ package internalfake
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserCredentials
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
import app.dapk.st.matrix.sync.internal.sync.LookupResult
@ -18,9 +19,16 @@ internal class FakeRoomEventCreator {
coEvery { with(instance) { event.toRoomEvent(roomId) } } returns result
}
fun givenCreatesUsingLookup(roomId: RoomId, eventIdToLookup: EventId, event: ApiTimelineEvent.TimelineMessage, result: RoomEvent, lookupResult: LookupResult) {
fun givenCreatesUsingLookup(
userCredentials: UserCredentials,
roomId: RoomId,
eventIdToLookup: EventId,
event: ApiTimelineEvent.TimelineMessage,
result: RoomEvent,
lookupResult: LookupResult
) {
val slot = slot<suspend (EventId) -> LookupResult>()
coEvery { with(instance) { event.toRoomEvent(roomId, capture(slot)) } } answers {
coEvery { with(instance) { event.toRoomEvent(userCredentials, roomId, capture(slot)) } } answers {
runBlocking {
if (slot.captured.invoke(eventIdToLookup) == lookupResult) {
result

View File

@ -1,5 +1,6 @@
package internalfake
import app.dapk.st.matrix.common.UserCredentials
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
import io.mockk.coEvery
@ -8,7 +9,7 @@ import io.mockk.mockk
internal class FakeRoomEventsDecrypter {
val instance = mockk<RoomEventsDecrypter>()
fun givenDecrypts(previousEvents: List<RoomEvent>, result: List<RoomEvent> = previousEvents) {
coEvery { instance.decryptRoomEvents(previousEvents) } returns result
fun givenDecrypts(userCredentials: UserCredentials, previousEvents: List<RoomEvent>, result: List<RoomEvent> = previousEvents) {
coEvery { instance.decryptRoomEvents(userCredentials, previousEvents) } returns result
}
}

View File

@ -39,9 +39,8 @@ internal fun anApiTimelineTextEvent(
internal fun aTimelineTextEventContent(
body: String? = null,
formattedBody: String? = null,
type: String? = null,
relation: ApiTimelineEvent.TimelineMessage.Relation? = null,
) = ApiTimelineEvent.TimelineMessage.Content(body, formattedBody, type, relation)
) = ApiTimelineEvent.TimelineMessage.Content.Text(body, formattedBody, relation)
internal fun anEditRelation(originalId: EventId) = ApiTimelineEvent.TimelineMessage.Relation(
relationType = "m.replace",

View File

@ -15,8 +15,8 @@ fun aRoomMessageEvent(
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, encryptedContent, edited)
fun aRoomReplyMessageEvent(
message: RoomEvent.Message = aRoomMessageEvent(),
replyingTo: RoomEvent.Message = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")),
message: RoomEvent = aRoomMessageEvent(),
replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")),
) = RoomEvent.Reply(message, replyingTo)
fun anEncryptedRoomMessageEvent(