Merge pull request #20 from ouchadam/support-image-messages
Support image messages
This commit is contained in:
commit
77efb58562
|
@ -0,0 +1,64 @@
|
|||
package app.dapk.st.messenger
|
||||
|
||||
import android.util.Base64
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.size.Size
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.Buffer
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
private const val CRYPTO_BUFFER_SIZE = 32 * 1024
|
||||
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
|
||||
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
|
||||
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
|
||||
|
||||
class DecryptingFetcher : Fetcher<RoomEvent.Image> {
|
||||
|
||||
private val http = OkHttpClient()
|
||||
|
||||
override suspend fun fetch(pool: BitmapPool, data: RoomEvent.Image, size: Size, options: Options): FetchResult {
|
||||
val keys = data.imageMeta.keys!!
|
||||
val key = Base64.decode(keys.k.replace('-', '+').replace('_', '/'), Base64.DEFAULT)
|
||||
val initVectorBytes = Base64.decode(keys.iv, Base64.DEFAULT)
|
||||
|
||||
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
|
||||
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
|
||||
val ivParameterSpec = IvParameterSpec(initVectorBytes)
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
|
||||
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
|
||||
|
||||
var read: Int
|
||||
val d = ByteArray(CRYPTO_BUFFER_SIZE)
|
||||
var decodedBytes: ByteArray
|
||||
|
||||
val response = http.newCall(Request.Builder().url(data.imageMeta.url).build()).execute()
|
||||
val outputStream = Buffer()
|
||||
|
||||
response.body()?.let {
|
||||
it.byteStream().use {
|
||||
read = it.read(d)
|
||||
while (read != -1) {
|
||||
messageDigest.update(d, 0, read)
|
||||
decodedBytes = decryptCipher.update(d, 0, read)
|
||||
outputStream.write(decodedBytes)
|
||||
read = it.read(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
return SourceResult(outputStream, null, DataSource.NETWORK)
|
||||
}
|
||||
|
||||
override fun key(data: RoomEvent.Image) = data.imageMeta.url
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ import androidx.activity.compose.setContent
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.dapk.st.design.components.SmallTalkTheme
|
||||
import app.dapk.st.core.DapkActivity
|
||||
import app.dapk.st.core.module
|
||||
import app.dapk.st.core.viewModel
|
||||
import app.dapk.st.design.components.SmallTalkTheme
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package app.dapk.st.messenger
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
@ -36,6 +37,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,30 +157,33 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState) {
|
|||
items = state.events,
|
||||
key = { _, item -> item.eventId.value },
|
||||
) { index, item ->
|
||||
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
|
||||
}
|
||||
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)
|
||||
val previousEvent = if (index != 0) state.events[index - 1] else null
|
||||
val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id
|
||||
AlignedBubble(item, self, wasPreviousMessageSameSender) {
|
||||
when (item) {
|
||||
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
|
||||
is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
|
||||
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class BubbleContent<T : RoomEvent>(
|
||||
val shape: RoundedCornerShape,
|
||||
val background: Color,
|
||||
val isNotSelf: Boolean,
|
||||
val message: T
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) {
|
||||
private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
|
||||
message: T,
|
||||
self: UserId,
|
||||
wasPreviousMessageSameSender: Boolean,
|
||||
content: @Composable (BubbleContent<T>) -> Unit
|
||||
) {
|
||||
when (message.author.id == self) {
|
||||
true -> {
|
||||
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) {
|
||||
|
@ -188,7 +193,7 @@ private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMes
|
|||
isNotSelf = false,
|
||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
||||
) {
|
||||
TextBubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message)
|
||||
content(BubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +205,7 @@ private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMes
|
|||
isNotSelf = true,
|
||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
||||
) {
|
||||
TextBubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message)
|
||||
content(BubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -208,42 +213,67 @@ private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMes
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyItemScope.Reply(self: UserId, message: RoomEvent.Reply, wasPreviousMessageSameSender: Boolean) {
|
||||
when (message.message.author.id == self) {
|
||||
true -> {
|
||||
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) {
|
||||
Box(modifier = Modifier.fillParentMaxWidth(0.85f), contentAlignment = Alignment.TopEnd) {
|
||||
Bubble(
|
||||
message = message.message,
|
||||
isNotSelf = false,
|
||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
||||
) {
|
||||
ReplyBubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message)
|
||||
}
|
||||
private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
|
||||
Box(modifier = Modifier.padding(start = 6.dp)) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.clip(content.shape)
|
||||
.background(content.background)
|
||||
.height(IntrinsicSize.Max),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.width(IntrinsicSize.Max)
|
||||
.defaultMinSize(minWidth = 50.dp)
|
||||
) {
|
||||
if (content.isNotSelf) {
|
||||
Text(
|
||||
fontSize = 11.sp,
|
||||
text = content.message.author.displayName ?: content.message.author.id.value,
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colors.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
Box(modifier = Modifier.fillParentMaxWidth(0.95f), contentAlignment = Alignment.TopStart) {
|
||||
Bubble(
|
||||
message = message.message,
|
||||
isNotSelf = true,
|
||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
||||
) {
|
||||
ReplyBubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message)
|
||||
|
||||
val width = with(LocalDensity.current) { content.message.imageMeta.width.toDp() }
|
||||
val height = with(LocalDensity.current) { content.message.imageMeta.height.toDp() }
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Image(
|
||||
modifier = Modifier.size(width, height),
|
||||
painter = rememberImagePainter(
|
||||
data = content.message,
|
||||
builder = { fetcher(DecryptingFetcher()) }
|
||||
),
|
||||
contentDescription = null,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
val editedPrefix = if (content.message.edited) "(edited) " else null
|
||||
Text(
|
||||
fontSize = 9.sp,
|
||||
text = "${editedPrefix ?: ""}${content.message.time}",
|
||||
textAlign = TextAlign.End,
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
modifier = Modifier.wrapContentSize()
|
||||
)
|
||||
SendStatus(content.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private val selfBackgroundShape = RoundedCornerShape(12.dp, 0.dp, 12.dp, 12.dp)
|
||||
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
|
||||
|
@ -270,13 +300,13 @@ private fun Bubble(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNotSelf: Boolean, message: Message) {
|
||||
private fun TextBubbleContent(content: BubbleContent<RoomEvent.Message>) {
|
||||
Box(modifier = Modifier.padding(start = 6.dp)) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.clip(shape)
|
||||
.background(background)
|
||||
.clip(content.shape)
|
||||
.background(content.background)
|
||||
.height(IntrinsicSize.Max),
|
||||
) {
|
||||
Column(
|
||||
|
@ -285,16 +315,16 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
|
|||
.width(IntrinsicSize.Max)
|
||||
.defaultMinSize(minWidth = 50.dp)
|
||||
) {
|
||||
if (isNotSelf) {
|
||||
if (content.isNotSelf) {
|
||||
Text(
|
||||
fontSize = 11.sp,
|
||||
text = message.author.displayName ?: message.author.id.value,
|
||||
text = content.message.author.displayName ?: content.message.author.id.value,
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colors.onPrimary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = message.content,
|
||||
text = content.message.content,
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 15.sp,
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
|
@ -303,15 +333,15 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
|
|||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
val editedPrefix = if (message.edited) "(edited) " else null
|
||||
val editedPrefix = if (content.message.edited) "(edited) " else null
|
||||
Text(
|
||||
fontSize = 9.sp,
|
||||
text = "${editedPrefix ?: ""}${message.time}",
|
||||
text = "${editedPrefix ?: ""}${content.message.time}",
|
||||
textAlign = TextAlign.End,
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
modifier = Modifier.wrapContentSize()
|
||||
)
|
||||
SendStatus(message)
|
||||
SendStatus(content.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -319,13 +349,13 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isNotSelf: Boolean, reply: RoomEvent.Reply) {
|
||||
private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||
Box(modifier = Modifier.padding(start = 6.dp)) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.clip(shape)
|
||||
.background(background)
|
||||
.clip(content.shape)
|
||||
.background(content.background)
|
||||
.height(IntrinsicSize.Max),
|
||||
) {
|
||||
Column(
|
||||
|
@ -336,53 +366,90 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN
|
|||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.background(if (isNotSelf) SmallTalkTheme.extendedColors.otherBubbleReplyBackground else SmallTalkTheme.extendedColors.selfBubbleReplyBackground)
|
||||
.background(if (content.isNotSelf) SmallTalkTheme.extendedColors.otherBubbleReplyBackground else SmallTalkTheme.extendedColors.selfBubbleReplyBackground)
|
||||
.padding(4.dp)
|
||||
) {
|
||||
val replyName = if (!isNotSelf && reply.replyingToSelf) "You" else reply.replyingTo.author.displayName ?: reply.replyingTo.author.id.value
|
||||
val replyName = if (!content.isNotSelf && content.message.replyingToSelf) "You" else content.message.replyingTo.author.displayName
|
||||
?: content.message.replyingTo.author.id.value
|
||||
Text(
|
||||
fontSize = 11.sp,
|
||||
text = replyName,
|
||||
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 = content.message.replyingTo) {
|
||||
is Message -> {
|
||||
Text(
|
||||
text = replyingTo.content,
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 15.sp,
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
is RoomEvent.Image -> {
|
||||
val width = with(LocalDensity.current) { replyingTo.imageMeta.width.toDp() }
|
||||
val height = with(LocalDensity.current) { replyingTo.imageMeta.height.toDp() }
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Image(
|
||||
modifier = Modifier.size(width, height),
|
||||
painter = rememberImagePainter(
|
||||
data = replyingTo,
|
||||
builder = { fetcher(DecryptingFetcher()) }
|
||||
),
|
||||
contentDescription = null,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
if (isNotSelf) {
|
||||
if (content.isNotSelf) {
|
||||
Text(
|
||||
fontSize = 11.sp,
|
||||
text = reply.message.author.displayName ?: reply.message.author.id.value,
|
||||
text = content.message.message.author.displayName ?: content.message.message.author.id.value,
|
||||
maxLines = 1,
|
||||
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 = content.message.message) {
|
||||
is Message -> {
|
||||
Text(
|
||||
text = message.content,
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 15.sp,
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
is RoomEvent.Image -> {
|
||||
val width = with(LocalDensity.current) { message.imageMeta.width.toDp() }
|
||||
val height = with(LocalDensity.current) { message.imageMeta.height.toDp() }
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Image(
|
||||
modifier = Modifier.size(width, height),
|
||||
painter = rememberImagePainter(
|
||||
data = content.message,
|
||||
builder = { fetcher(DecryptingFetcher()) }
|
||||
),
|
||||
contentDescription = null,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
fontSize = 9.sp,
|
||||
text = reply.time,
|
||||
text = content.message.time,
|
||||
textAlign = TextAlign.End,
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
modifier = Modifier.wrapContentSize()
|
||||
)
|
||||
SendStatus(reply.message)
|
||||
SendStatus(content.message.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -391,7 +458,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
|
||||
|
|
|
@ -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,59 @@ 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,
|
||||
@SerialName("keys") val keys: Keys?,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Keys(
|
||||
@SerialName("k") val k: String,
|
||||
@SerialName("iv") val iv: String,
|
||||
@SerialName("v") val v: String,
|
||||
@SerialName("hashes") val hashes: Map<String, String>,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -445,13 +445,55 @@ 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("url") val url: MxUrl? = null,
|
||||
@SerialName("file") val file: File? = null,
|
||||
@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,
|
||||
@SerialName("iv") val iv: String,
|
||||
@SerialName("v") val v: String,
|
||||
@SerialName("hashes") val hashes: Map<String, String>,
|
||||
@SerialName("key") val key: Key,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Key(
|
||||
@SerialName("k") val k: String,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@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 +554,28 @@ 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" -> 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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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,38 @@ 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) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer),
|
||||
keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) }
|
||||
),
|
||||
encryptedContent = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,10 +61,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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
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) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer),
|
||||
keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue