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.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.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import app.dapk.st.design.components.SmallTalkTheme
|
|
||||||
import app.dapk.st.core.DapkActivity
|
import app.dapk.st.core.DapkActivity
|
||||||
import app.dapk.st.core.module
|
import app.dapk.st.core.module
|
||||||
import app.dapk.st.core.viewModel
|
import app.dapk.st.core.viewModel
|
||||||
|
import app.dapk.st.design.components.SmallTalkTheme
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package app.dapk.st.messenger
|
package app.dapk.st.messenger
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.*
|
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.RoomEvent.Message
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
import app.dapk.st.matrix.sync.RoomState
|
||||||
import app.dapk.st.navigator.Navigator
|
import app.dapk.st.navigator.Navigator
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -155,30 +157,33 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState) {
|
||||||
items = state.events,
|
items = state.events,
|
||||||
key = { _, item -> item.eventId.value },
|
key = { _, item -> item.eventId.value },
|
||||||
) { index, item ->
|
) { index, item ->
|
||||||
when (item) {
|
val previousEvent = if (index != 0) state.events[index - 1] else null
|
||||||
is Message -> {
|
val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id
|
||||||
val wasPreviousMessageSameSender = when (val previousEvent = if (index != 0) state.events[index - 1] else null) {
|
AlignedBubble(item, self, wasPreviousMessageSameSender) {
|
||||||
null -> false
|
when (item) {
|
||||||
is Message -> previousEvent.author.id == item.author.id
|
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
|
||||||
is RoomEvent.Reply -> previousEvent.message.author.id == item.author.id
|
is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
|
||||||
}
|
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class BubbleContent<T : RoomEvent>(
|
||||||
|
val shape: RoundedCornerShape,
|
||||||
|
val background: Color,
|
||||||
|
val isNotSelf: Boolean,
|
||||||
|
val message: T
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@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) {
|
when (message.author.id == self) {
|
||||||
true -> {
|
true -> {
|
||||||
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) {
|
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) {
|
||||||
|
@ -188,7 +193,7 @@ private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMes
|
||||||
isNotSelf = false,
|
isNotSelf = false,
|
||||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
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,
|
isNotSelf = true,
|
||||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
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
|
@Composable
|
||||||
private fun LazyItemScope.Reply(self: UserId, message: RoomEvent.Reply, wasPreviousMessageSameSender: Boolean) {
|
private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
|
||||||
when (message.message.author.id == self) {
|
Box(modifier = Modifier.padding(start = 6.dp)) {
|
||||||
true -> {
|
Box(
|
||||||
Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) {
|
Modifier
|
||||||
Box(modifier = Modifier.fillParentMaxWidth(0.85f), contentAlignment = Alignment.TopEnd) {
|
.padding(4.dp)
|
||||||
Bubble(
|
.clip(content.shape)
|
||||||
message = message.message,
|
.background(content.background)
|
||||||
isNotSelf = false,
|
.height(IntrinsicSize.Max),
|
||||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
) {
|
||||||
) {
|
Column(
|
||||||
ReplyBubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message)
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
val width = with(LocalDensity.current) { content.message.imageMeta.width.toDp() }
|
||||||
false -> {
|
val height = with(LocalDensity.current) { content.message.imageMeta.height.toDp() }
|
||||||
Box(modifier = Modifier.fillParentMaxWidth(0.95f), contentAlignment = Alignment.TopStart) {
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Bubble(
|
Image(
|
||||||
message = message.message,
|
modifier = Modifier.size(width, height),
|
||||||
isNotSelf = true,
|
painter = rememberImagePainter(
|
||||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
data = content.message,
|
||||||
) {
|
builder = { fetcher(DecryptingFetcher()) }
|
||||||
ReplyBubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message)
|
),
|
||||||
|
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 selfBackgroundShape = RoundedCornerShape(12.dp, 0.dp, 12.dp, 12.dp)
|
||||||
private val othersBackgroundShape = RoundedCornerShape(0.dp, 12.dp, 12.dp, 12.dp)
|
private val othersBackgroundShape = RoundedCornerShape(0.dp, 12.dp, 12.dp, 12.dp)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Bubble(
|
private fun Bubble(
|
||||||
message: Message,
|
message: RoomEvent,
|
||||||
isNotSelf: Boolean,
|
isNotSelf: Boolean,
|
||||||
wasPreviousMessageSameSender: Boolean,
|
wasPreviousMessageSameSender: Boolean,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
|
@ -270,13 +300,13 @@ private fun Bubble(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 = Modifier.padding(start = 6.dp)) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.clip(shape)
|
.clip(content.shape)
|
||||||
.background(background)
|
.background(content.background)
|
||||||
.height(IntrinsicSize.Max),
|
.height(IntrinsicSize.Max),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
@ -285,16 +315,16 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
|
||||||
.width(IntrinsicSize.Max)
|
.width(IntrinsicSize.Max)
|
||||||
.defaultMinSize(minWidth = 50.dp)
|
.defaultMinSize(minWidth = 50.dp)
|
||||||
) {
|
) {
|
||||||
if (isNotSelf) {
|
if (content.isNotSelf) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
text = message.author.displayName ?: message.author.id.value,
|
text = content.message.author.displayName ?: content.message.author.id.value,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = MaterialTheme.colors.onPrimary
|
color = MaterialTheme.colors.onPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = message.content,
|
text = content.message.content,
|
||||||
color = MaterialTheme.colors.onPrimary,
|
color = MaterialTheme.colors.onPrimary,
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
modifier = Modifier.wrapContentSize(),
|
modifier = Modifier.wrapContentSize(),
|
||||||
|
@ -303,15 +333,15 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
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(
|
Text(
|
||||||
fontSize = 9.sp,
|
fontSize = 9.sp,
|
||||||
text = "${editedPrefix ?: ""}${message.time}",
|
text = "${editedPrefix ?: ""}${content.message.time}",
|
||||||
textAlign = TextAlign.End,
|
textAlign = TextAlign.End,
|
||||||
color = MaterialTheme.colors.onPrimary,
|
color = MaterialTheme.colors.onPrimary,
|
||||||
modifier = Modifier.wrapContentSize()
|
modifier = Modifier.wrapContentSize()
|
||||||
)
|
)
|
||||||
SendStatus(message)
|
SendStatus(content.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -319,13 +349,13 @@ private fun TextBubbleContent(shape: RoundedCornerShape, background: Color, isNo
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 = Modifier.padding(start = 6.dp)) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.clip(shape)
|
.clip(content.shape)
|
||||||
.background(background)
|
.background(content.background)
|
||||||
.height(IntrinsicSize.Max),
|
.height(IntrinsicSize.Max),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
@ -336,53 +366,90 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
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)
|
.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(
|
Text(
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
text = replyName,
|
text = replyName,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = MaterialTheme.colors.onPrimary
|
color = MaterialTheme.colors.onPrimary
|
||||||
)
|
)
|
||||||
Text(
|
when (val replyingTo = content.message.replyingTo) {
|
||||||
text = reply.replyingTo.content,
|
is Message -> {
|
||||||
color = MaterialTheme.colors.onPrimary,
|
Text(
|
||||||
fontSize = 15.sp,
|
text = replyingTo.content,
|
||||||
modifier = Modifier.wrapContentSize(),
|
color = MaterialTheme.colors.onPrimary,
|
||||||
textAlign = TextAlign.Start,
|
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))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
if (isNotSelf) {
|
if (content.isNotSelf) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 11.sp,
|
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,
|
maxLines = 1,
|
||||||
color = MaterialTheme.colors.onPrimary
|
color = MaterialTheme.colors.onPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
when (val message = content.message.message) {
|
||||||
text = reply.message.content,
|
is Message -> {
|
||||||
color = MaterialTheme.colors.onPrimary,
|
Text(
|
||||||
fontSize = 15.sp,
|
text = message.content,
|
||||||
modifier = Modifier.wrapContentSize(),
|
color = MaterialTheme.colors.onPrimary,
|
||||||
textAlign = TextAlign.Start,
|
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))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 9.sp,
|
fontSize = 9.sp,
|
||||||
text = reply.time,
|
text = content.message.time,
|
||||||
textAlign = TextAlign.End,
|
textAlign = TextAlign.End,
|
||||||
color = MaterialTheme.colors.onPrimary,
|
color = MaterialTheme.colors.onPrimary,
|
||||||
modifier = Modifier.wrapContentSize()
|
modifier = Modifier.wrapContentSize()
|
||||||
)
|
)
|
||||||
SendStatus(reply.message)
|
SendStatus(content.message.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -391,7 +458,7 @@ private fun ReplyBubbleContent(shape: RoundedCornerShape, background: Color, isN
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RowScope.SendStatus(message: Message) {
|
private fun RowScope.SendStatus(message: RoomEvent) {
|
||||||
when (val meta = message.meta) {
|
when (val meta = message.meta) {
|
||||||
MessageMeta.FromServer -> {
|
MessageMeta.FromServer -> {
|
||||||
// last message is self
|
// last message is self
|
||||||
|
|
|
@ -4,7 +4,6 @@ import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.matrix.common.*
|
import app.dapk.st.matrix.common.*
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
@ -23,6 +22,8 @@ sealed class RoomEvent {
|
||||||
|
|
||||||
abstract val eventId: EventId
|
abstract val eventId: EventId
|
||||||
abstract val utcTimestamp: Long
|
abstract val utcTimestamp: Long
|
||||||
|
abstract val author: RoomMember
|
||||||
|
abstract val meta: MessageMeta
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("message")
|
@SerialName("message")
|
||||||
|
@ -30,8 +31,8 @@ sealed class RoomEvent {
|
||||||
@SerialName("event_id") override val eventId: EventId,
|
@SerialName("event_id") override val eventId: EventId,
|
||||||
@SerialName("timestamp") override val utcTimestamp: Long,
|
@SerialName("timestamp") override val utcTimestamp: Long,
|
||||||
@SerialName("content") val content: String,
|
@SerialName("content") val content: String,
|
||||||
@SerialName("author") val author: RoomMember,
|
@SerialName("author") override val author: RoomMember,
|
||||||
@SerialName("meta") val meta: MessageMeta,
|
@SerialName("meta") override val meta: MessageMeta,
|
||||||
@SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null,
|
@SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null,
|
||||||
@SerialName("edited") val edited: Boolean = false,
|
@SerialName("edited") val edited: Boolean = false,
|
||||||
) : RoomEvent() {
|
) : RoomEvent() {
|
||||||
|
@ -44,7 +45,6 @@ sealed class RoomEvent {
|
||||||
@SerialName("session_id") val sessionId: SessionId,
|
@SerialName("session_id") val sessionId: SessionId,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Transient
|
|
||||||
val time: String by unsafeLazy {
|
val time: String by unsafeLazy {
|
||||||
val instant = Instant.ofEpochMilli(utcTimestamp)
|
val instant = Instant.ofEpochMilli(utcTimestamp)
|
||||||
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
|
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
|
||||||
|
@ -54,22 +54,59 @@ sealed class RoomEvent {
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("reply")
|
@SerialName("reply")
|
||||||
data class Reply(
|
data class Reply(
|
||||||
@SerialName("message") val message: Message,
|
@SerialName("message") val message: RoomEvent,
|
||||||
@SerialName("in_reply_to") val replyingTo: Message,
|
@SerialName("in_reply_to") val replyingTo: RoomEvent,
|
||||||
) : RoomEvent() {
|
) : RoomEvent() {
|
||||||
|
|
||||||
override val eventId: EventId = message.eventId
|
override val eventId: EventId = message.eventId
|
||||||
override val utcTimestamp: Long = message.utcTimestamp
|
override val utcTimestamp: Long = message.utcTimestamp
|
||||||
|
override val author: RoomMember = message.author
|
||||||
|
override val meta: MessageMeta = message.meta
|
||||||
|
|
||||||
val replyingToSelf = replyingTo.author == message.author
|
val replyingToSelf = replyingTo.author == message.author
|
||||||
|
|
||||||
@Transient
|
|
||||||
val time: String by unsafeLazy {
|
val time: String by unsafeLazy {
|
||||||
val instant = Instant.ofEpochMilli(utcTimestamp)
|
val instant = Instant.ofEpochMilli(utcTimestamp)
|
||||||
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
|
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
|
@Serializable
|
||||||
|
|
|
@ -35,7 +35,7 @@ internal class DefaultSyncService(
|
||||||
json: Json,
|
json: Json,
|
||||||
oneTimeKeyProducer: MaybeCreateMoreKeys,
|
oneTimeKeyProducer: MaybeCreateMoreKeys,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
credentialsStore: CredentialsStore,
|
private val credentialsStore: CredentialsStore,
|
||||||
roomMembersService: RoomMembersService,
|
roomMembersService: RoomMembersService,
|
||||||
logger: MatrixLogger,
|
logger: MatrixLogger,
|
||||||
errorTracker: ErrorTracker,
|
errorTracker: ErrorTracker,
|
||||||
|
@ -57,7 +57,7 @@ internal class DefaultSyncService(
|
||||||
roomMembersService,
|
roomMembersService,
|
||||||
roomDataSource,
|
roomDataSource,
|
||||||
TimelineEventsProcessor(
|
TimelineEventsProcessor(
|
||||||
RoomEventCreator(roomMembersService, logger, errorTracker),
|
RoomEventCreator(roomMembersService, errorTracker, RoomEventFactory(roomMembersService)),
|
||||||
roomEventsDecrypter,
|
roomEventsDecrypter,
|
||||||
eventDecrypter,
|
eventDecrypter,
|
||||||
EventLookupUseCase(roomStore)
|
EventLookupUseCase(roomStore)
|
||||||
|
@ -69,6 +69,7 @@ internal class DefaultSyncService(
|
||||||
roomRefresher,
|
roomRefresher,
|
||||||
roomDataSource,
|
roomDataSource,
|
||||||
logger,
|
logger,
|
||||||
|
errorTracker,
|
||||||
coroutineDispatchers,
|
coroutineDispatchers,
|
||||||
)
|
)
|
||||||
SyncUseCase(
|
SyncUseCase(
|
||||||
|
@ -114,7 +115,7 @@ internal class DefaultSyncService(
|
||||||
coroutineDispatchers.withIoContext {
|
coroutineDispatchers.withIoContext {
|
||||||
roomIds.map {
|
roomIds.map {
|
||||||
async {
|
async {
|
||||||
roomRefresher.refreshRoomContent(it)?.also {
|
roomRefresher.refreshRoomContent(it, credentialsStore.credentials()!!)?.also {
|
||||||
overviewStore.persist(listOf(it.roomOverview))
|
overviewStore.persist(listOf(it.roomOverview))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -445,13 +445,55 @@ internal sealed class ApiTimelineEvent {
|
||||||
@SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null
|
@SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null
|
||||||
) : ApiTimelineEvent() {
|
) : ApiTimelineEvent() {
|
||||||
|
|
||||||
@Serializable
|
@Serializable(with = ApiTimelineMessageContentDeserializer::class)
|
||||||
internal data class Content(
|
internal sealed interface Content {
|
||||||
@SerialName("body") val body: String? = null,
|
val relation: Relation?
|
||||||
@SerialName("formatted_body") val formattedBody: String? = null,
|
|
||||||
@SerialName("msgtype") val type: String? = null,
|
@Serializable
|
||||||
@SerialName("m.relates_to") val relation: Relation? = null,
|
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
|
@Serializable
|
||||||
data class Relation(
|
data class Relation(
|
||||||
|
@ -512,3 +554,28 @@ internal object EncryptedContentDeserializer : KSerializer<ApiEncryptedContent>
|
||||||
override fun serialize(encoder: Encoder, value: ApiEncryptedContent) = TODO("Not yet implemented")
|
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.common.*
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
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 app.dapk.st.matrix.sync.internal.request.DecryptedContent
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@ -11,13 +12,38 @@ internal class RoomEventsDecrypter(
|
||||||
private val logger: MatrixLogger,
|
private val logger: MatrixLogger,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun decryptRoomEvents(events: List<RoomEvent>) = events.map { event ->
|
suspend fun decryptRoomEvents(userCredentials: UserCredentials, events: List<RoomEvent>) = events.map { event ->
|
||||||
when (event) {
|
decryptEvent(event, userCredentials)
|
||||||
is RoomEvent.Message -> event.decrypt()
|
}
|
||||||
is RoomEvent.Reply -> RoomEvent.Reply(
|
|
||||||
message = event.message.decrypt(),
|
private suspend fun decryptEvent(event: RoomEvent, userCredentials: UserCredentials): RoomEvent = when (event) {
|
||||||
replyingTo = event.replyingTo.decrypt(),
|
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 JsonString.toModel() = json.decodeFromString(DecryptedContent.serializer(), this.value)
|
||||||
|
|
||||||
private fun RoomEvent.Message.copyWithDecryptedContent(decryptedContent: DecryptedContent.TimelineText) = this.copy(
|
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
|
encryptedContent = null
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomEvent.Message.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1(
|
private fun RoomEvent.Message.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1(
|
||||||
|
|
|
@ -39,7 +39,11 @@ internal class SyncEventDecrypter(
|
||||||
is DecryptedContent.TimelineText -> ApiTimelineEvent.TimelineMessage(
|
is DecryptedContent.TimelineText -> ApiTimelineEvent.TimelineMessage(
|
||||||
event.eventId,
|
event.eventId,
|
||||||
event.senderId,
|
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,
|
event.utcTimestamp,
|
||||||
).also { logger.matrixLog("decrypted to timeline text: $it") }
|
).also { logger.matrixLog("decrypted to timeline text: $it") }
|
||||||
DecryptedContent.Ignored -> event
|
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.ifOrNull
|
||||||
import app.dapk.st.core.extensions.nullAndTrack
|
import app.dapk.st.core.extensions.nullAndTrack
|
||||||
import app.dapk.st.matrix.common.EventId
|
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.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.MessageMeta
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
import app.dapk.st.matrix.sync.RoomMembersService
|
import app.dapk.st.matrix.sync.RoomMembersService
|
||||||
|
@ -18,8 +17,8 @@ private typealias Lookup = suspend (EventId) -> LookupResult
|
||||||
|
|
||||||
internal class RoomEventCreator(
|
internal class RoomEventCreator(
|
||||||
private val roomMembersService: RoomMembersService,
|
private val roomMembersService: RoomMembersService,
|
||||||
private val logger: MatrixLogger,
|
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
|
private val roomEventFactory: RoomEventFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun ApiTimelineEvent.Encrypted.toRoomEvent(roomId: RoomId): RoomEvent? {
|
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 {
|
return when {
|
||||||
this.isEdit() -> handleEdit(roomId, this.content.relation!!.eventId!!, lookup)
|
event.content == ApiTimelineEvent.TimelineMessage.Content.Ignored -> null
|
||||||
this.isReply() -> handleReply(roomId, lookup)
|
event.isEdit() -> event.handleEdit(editedEventId = event.content.relation!!.eventId!!, lookup)
|
||||||
else -> this.toMessage(roomId)
|
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? {
|
private suspend fun ApiTimelineEvent.TimelineMessage.handleReply(replyToId: EventId, lookup: Lookup): RoomEvent {
|
||||||
return lookup(editedEventId).fold(
|
val relationEvent = lookup(replyToId).fold(
|
||||||
onApiTimelineEvent = {
|
onApiTimelineEvent = { it.toTextMessage() },
|
||||||
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) },
|
|
||||||
onRoomEvent = { it },
|
onRoomEvent = { it },
|
||||||
onEmpty = { null }
|
onEmpty = { null }
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.matrixLog("found relation: $relationEvent")
|
|
||||||
|
|
||||||
return when (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 -> {
|
else -> {
|
||||||
RoomEvent.Reply(
|
RoomEvent.Reply(
|
||||||
message = this.toMessage(roomId, content = this.content.formattedBody?.stripTags() ?: "redacted"),
|
message = roomEventFactory.mapToRoomEvent(this),
|
||||||
replyingTo = when (relationEvent) {
|
replyingTo = when (relationEvent) {
|
||||||
is RoomEvent.Message -> relationEvent
|
is RoomEvent.Message -> relationEvent
|
||||||
is RoomEvent.Reply -> relationEvent.message
|
is RoomEvent.Reply -> relationEvent.message
|
||||||
|
is RoomEvent.Image -> relationEvent
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ApiTimelineEvent.TimelineMessage.toMessage(
|
private suspend fun ApiTimelineEvent.TimelineMessage.toFallbackTextMessage() = this.toTextMessage(content = this.asTextContent().body ?: "redacted")
|
||||||
roomId: RoomId,
|
|
||||||
content: String = this.content.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,
|
edited: Boolean = false,
|
||||||
utcTimestamp: Long = this.utcTimestamp,
|
utcTimestamp: Long = this.utcTimestamp,
|
||||||
) = RoomEvent.Message(
|
) = with(roomEventFactory) { toTextMessage(roomId, content, edited, utcTimestamp) }
|
||||||
eventId = this.id,
|
|
||||||
content = content,
|
private suspend fun ApiTimelineEvent.TimelineMessage.toImageMessage(
|
||||||
author = roomMembersService.find(roomId, this.senderId)!!,
|
edited: Boolean = false,
|
||||||
utcTimestamp = utcTimestamp,
|
utcTimestamp: Long = this.utcTimestamp,
|
||||||
meta = MessageMeta.FromServer,
|
) = with(roomEventFactory) { toImageMessage(userCredentials, roomId, edited, utcTimestamp) }
|
||||||
edited = edited,
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,5 +167,6 @@ private fun String.stripTags() = this.substring(this.indexOf("</mx-reply>") + "<
|
||||||
.replace("<em>", "")
|
.replace("<em>", "")
|
||||||
.replace("</em>", "")
|
.replace("</em>", "")
|
||||||
|
|
||||||
private fun ApiTimelineEvent.TimelineMessage.isEdit() = this.content.relation?.relationType == "m.replace" && this.content.relation.eventId != 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.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
|
package app.dapk.st.matrix.sync.internal.sync
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.MatrixLogTag
|
import app.dapk.st.matrix.common.*
|
||||||
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.sync.RoomEvent
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
import app.dapk.st.matrix.sync.RoomState
|
||||||
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
|
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
|
||||||
|
@ -14,13 +11,13 @@ internal class RoomRefresher(
|
||||||
private val logger: MatrixLogger
|
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")
|
logger.matrixLog(MatrixLogTag.SYNC, "reducing side effect: $roomId")
|
||||||
return when (val previousState = roomDataSource.read(roomId)) {
|
return when (val previousState = roomDataSource.read(roomId)) {
|
||||||
null -> null.also { logger.matrixLog(MatrixLogTag.SYNC, "no previous state to update") }
|
null -> null.also { logger.matrixLog(MatrixLogTag.SYNC, "no previous state to update") }
|
||||||
else -> {
|
else -> {
|
||||||
logger.matrixLog(MatrixLogTag.SYNC, "previous state updated")
|
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()
|
val lastMessage = decryptedEvents.sortedByDescending { it.utcTimestamp }.findLastMessage()
|
||||||
|
|
||||||
previousState.copy(events = decryptedEvents, roomOverview = previousState.roomOverview.copy(lastMessage = lastMessage)).also {
|
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
|
package app.dapk.st.matrix.sync.internal.sync
|
||||||
|
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.core.withIoContextAsync
|
import app.dapk.st.core.withIoContextAsync
|
||||||
import app.dapk.st.matrix.common.*
|
import app.dapk.st.matrix.common.*
|
||||||
import app.dapk.st.matrix.common.MatrixLogTag.SYNC
|
import app.dapk.st.matrix.common.MatrixLogTag.SYNC
|
||||||
|
@ -16,6 +17,7 @@ internal class SyncReducer(
|
||||||
private val roomRefresher: RoomRefresher,
|
private val roomRefresher: RoomRefresher,
|
||||||
private val roomDataSource: RoomDataSource,
|
private val roomDataSource: RoomDataSource,
|
||||||
private val logger: MatrixLogger,
|
private val logger: MatrixLogger,
|
||||||
|
private val errorTracker: ErrorTracker,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -48,14 +50,14 @@ internal class SyncReducer(
|
||||||
isInitialSync = isInitialSync
|
isInitialSync = isInitialSync
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onFailure { logger.matrixLog(SYNC, "failed to reduce: $roomId, skipping") }
|
.onFailure { errorTracker.track(it, "failed to reduce: $roomId, skipping") }
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
}
|
}
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
|
|
||||||
val roomsWithSideEffects = sideEffects.roomsToRefresh(alreadyHandledRooms = apiUpdatedRooms?.keys ?: emptySet()).map { roomId ->
|
val roomsWithSideEffects = sideEffects.roomsToRefresh(alreadyHandledRooms = apiUpdatedRooms?.keys ?: emptySet()).map { roomId ->
|
||||||
coroutineDispatchers.withIoContextAsync {
|
coroutineDispatchers.withIoContextAsync {
|
||||||
roomRefresher.refreshRoomContent(roomId)
|
roomRefresher.refreshRoomContent(roomId, userCredentials)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package app.dapk.st.matrix.sync.internal.sync
|
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.RoomEvent
|
||||||
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
||||||
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
|
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> {
|
private suspend fun processNewEvents(roomToProcess: RoomToProcess, previousEvents: List<RoomEvent>): List<RoomEvent> {
|
||||||
val decryptedTimeline = roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.decryptEvents()
|
val decryptedTimeline = roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.decryptEvents()
|
||||||
val decryptedPreviousEvents = previousEvents.decryptEvents()
|
val decryptedPreviousEvents = previousEvents.decryptEvents(roomToProcess.userCredentials)
|
||||||
|
|
||||||
val newEvents = with(roomEventCreator) {
|
val newEvents = with(roomEventCreator) {
|
||||||
decryptedTimeline.value.mapNotNull { event ->
|
decryptedTimeline.value.mapNotNull { event ->
|
||||||
val roomEvent = when (event) {
|
val roomEvent = when (event) {
|
||||||
is ApiTimelineEvent.Encrypted -> event.toRoomEvent(roomToProcess.roomId)
|
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)
|
eventLookupUseCase.lookup(eventId, decryptedTimeline, decryptedPreviousEvents)
|
||||||
}
|
}
|
||||||
is ApiTimelineEvent.Encryption -> null
|
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<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) {
|
when (it) {
|
||||||
is RoomEvent.Message -> it.author.id == selfId
|
is RoomEvent.Message -> it.author.id == selfId
|
||||||
is RoomEvent.Reply -> it.message.author.id == selfId
|
is RoomEvent.Reply -> it.message.author.id == selfId
|
||||||
|
is RoomEvent.Image -> it.author.id == selfId
|
||||||
}
|
}
|
||||||
}.map { it.eventId }
|
}.map { it.eventId }
|
||||||
roomStore.insertUnread(overview.roomId, eventsFromOthers)
|
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"))
|
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_DECRYPTED_CONTENT = DecryptedContent.TimelineText(aTimelineTextEventContent(body = A_DECRYPTED_MESSAGE_CONTENT))
|
||||||
|
private val A_USER_CREDENTIALS = aUserCredentials()
|
||||||
|
|
||||||
class RoomEventsDecrypterTest {
|
class RoomEventsDecrypterTest {
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ class RoomEventsDecrypterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given clear message event, when decrypting, then does nothing`() = runTest {
|
fun `given clear message event, when decrypting, then does nothing`() = runTest {
|
||||||
val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null)
|
val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null)
|
||||||
val result = roomEventsDecrypter.decryptRoomEvents(listOf(aClearMessageEvent))
|
val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent))
|
||||||
|
|
||||||
result shouldBeEqualTo 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 {
|
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)
|
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))
|
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 {
|
fun `given encrypted reply event, when decrypting, then decrypts message and replyTo`() = runTest {
|
||||||
givenEncryptedReply(AN_ENCRYPTED_ROOM_REPLY, decryptsTo = A_DECRYPTED_CONTENT)
|
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(
|
result shouldBeEqualTo listOf(
|
||||||
AN_ENCRYPTED_ROOM_REPLY.copy(
|
AN_ENCRYPTED_ROOM_REPLY.copy(
|
||||||
message = AN_ENCRYPTED_ROOM_REPLY.message.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.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) {
|
private fun givenEncryptedMessage(roomMessage: RoomEvent.Message, decryptsTo: DecryptedContent) {
|
||||||
val model = roomMessage.encryptedContent!!.toModel()
|
val model = roomMessage.encryptedContent!!.toModel()
|
||||||
fakeMessageDecrypter.givenDecrypt(model)
|
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) {
|
private fun givenEncryptedReply(roomReply: RoomEvent.Reply, decryptsTo: DecryptedContent) {
|
||||||
givenEncryptedMessage(roomReply.message, decryptsTo)
|
givenEncryptedMessage(roomReply.message as RoomEvent.Message, decryptsTo)
|
||||||
givenEncryptedMessage(roomReply.replyingTo, decryptsTo)
|
givenEncryptedMessage(roomReply.replyingTo as RoomEvent.Message, decryptsTo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,12 +26,13 @@ private val A_TEXT_EVENT_WITHOUT_CONTENT = anApiTimelineTextEvent(
|
||||||
senderId = A_SENDER.id,
|
senderId = A_SENDER.id,
|
||||||
content = aTimelineTextEventContent(body = null)
|
content = aTimelineTextEventContent(body = null)
|
||||||
)
|
)
|
||||||
|
private val A_USER_CREDENTIALS = aUserCredentials()
|
||||||
|
|
||||||
internal class RoomEventCreatorTest {
|
internal class RoomEventCreatorTest {
|
||||||
|
|
||||||
private val fakeRoomMembersService = FakeRoomMembersService()
|
private val fakeRoomMembersService = FakeRoomMembersService()
|
||||||
|
|
||||||
private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeMatrixLogger(), FakeErrorTracker())
|
private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeErrorTracker(), RoomEventFactory(fakeRoomMembersService))
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given Megolm encrypted event then maps to encrypted room message`() = runTest {
|
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 {
|
fun `given text event then maps to room message`() = runTest {
|
||||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
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(
|
result shouldBeEqualTo aRoomMessageEvent(
|
||||||
eventId = A_TEXT_EVENT.id,
|
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 {
|
fun `given text event without body then maps to redacted room message`() = runTest {
|
||||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
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(
|
result shouldBeEqualTo aRoomMessageEvent(
|
||||||
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
|
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
|
||||||
|
@ -100,12 +101,12 @@ internal class RoomEventCreatorTest {
|
||||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||||
val editEvent = anApiTimelineTextEvent().toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
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(
|
result shouldBeEqualTo aRoomMessageEvent(
|
||||||
eventId = editEvent.id,
|
eventId = editEvent.id,
|
||||||
utcTimestamp = editEvent.utcTimestamp,
|
utcTimestamp = editEvent.utcTimestamp,
|
||||||
content = editEvent.content.body!!,
|
content = editEvent.asTextContent().body!!,
|
||||||
author = A_SENDER,
|
author = A_SENDER,
|
||||||
edited = true
|
edited = true
|
||||||
)
|
)
|
||||||
|
@ -118,7 +119,7 @@ internal class RoomEventCreatorTest {
|
||||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
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(
|
result shouldBeEqualTo aRoomMessageEvent(
|
||||||
eventId = originalMessage.id,
|
eventId = originalMessage.id,
|
||||||
|
@ -136,7 +137,7 @@ internal class RoomEventCreatorTest {
|
||||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
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(
|
result shouldBeEqualTo aRoomMessageEvent(
|
||||||
eventId = originalMessage.eventId,
|
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 {
|
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)
|
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||||
val originalMessage = aRoomReplyMessageEvent(message = aRoomMessageEvent())
|
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 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(
|
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||||
replyingTo = originalMessage.replyingTo,
|
replyingTo = originalMessage.replyingTo,
|
||||||
|
@ -174,7 +175,7 @@ internal class RoomEventCreatorTest {
|
||||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
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
|
result shouldBeEqualTo null
|
||||||
}
|
}
|
||||||
|
@ -185,22 +186,23 @@ internal class RoomEventCreatorTest {
|
||||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
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
|
result shouldBeEqualTo null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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)
|
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||||
val replyEvent = anApiTimelineTextEvent().toReplyEvent(messageContent = A_TEXT_EVENT_MESSAGE)
|
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(
|
result shouldBeEqualTo aRoomMessageEvent(
|
||||||
eventId = replyEvent.id,
|
eventId = replyEvent.id,
|
||||||
utcTimestamp = replyEvent.utcTimestamp,
|
utcTimestamp = replyEvent.utcTimestamp,
|
||||||
content = "${replyEvent.content.body}",
|
content = replyEvent.asTextContent().body!!,
|
||||||
author = A_SENDER,
|
author = A_SENDER,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -212,13 +214,13 @@ internal class RoomEventCreatorTest {
|
||||||
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
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(
|
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||||
replyingTo = aRoomMessageEvent(
|
replyingTo = aRoomMessageEvent(
|
||||||
eventId = originalMessage.id,
|
eventId = originalMessage.id,
|
||||||
utcTimestamp = originalMessage.utcTimestamp,
|
utcTimestamp = originalMessage.utcTimestamp,
|
||||||
content = originalMessage.content.body!!,
|
content = originalMessage.asTextContent().body!!,
|
||||||
author = A_SENDER,
|
author = A_SENDER,
|
||||||
),
|
),
|
||||||
message = aRoomMessageEvent(
|
message = aRoomMessageEvent(
|
||||||
|
@ -237,7 +239,7 @@ internal class RoomEventCreatorTest {
|
||||||
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
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(
|
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||||
replyingTo = originalMessage,
|
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 {
|
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)
|
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||||
val originalMessage = aRoomReplyMessageEvent()
|
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 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(
|
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||||
replyingTo = originalMessage.message,
|
replyingTo = originalMessage.message,
|
||||||
|
@ -326,4 +328,6 @@ private fun ApiEncryptedContent.toMegolm(): RoomEvent.Message.MegOlmV1 {
|
||||||
|
|
||||||
private class FakeLookup(private val result: LookupResult) : suspend (EventId) -> LookupResult {
|
private class FakeLookup(private val result: LookupResult) : suspend (EventId) -> LookupResult {
|
||||||
override suspend fun invoke(p1: EventId) = result
|
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 DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT)
|
||||||
val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS)
|
val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS)
|
||||||
}
|
}
|
||||||
|
private val A_USER_CREDENTIALS = aUserCredentials()
|
||||||
|
|
||||||
internal class RoomRefresherTest {
|
internal class RoomRefresherTest {
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ internal class RoomRefresherTest {
|
||||||
fun `given no existing room when refreshing then does nothing`() = runTest {
|
fun `given no existing room when refreshing then does nothing`() = runTest {
|
||||||
fakeRoomDataSource.givenNoCachedRoom(A_ROOM_ID)
|
fakeRoomDataSource.givenNoCachedRoom(A_ROOM_ID)
|
||||||
|
|
||||||
val result = roomRefresher.refreshRoomContent(aRoomId())
|
val result = roomRefresher.refreshRoomContent(aRoomId(), A_USER_CREDENTIALS)
|
||||||
|
|
||||||
result shouldBeEqualTo null
|
result shouldBeEqualTo null
|
||||||
fakeRoomDataSource.verifyNoChanges()
|
fakeRoomDataSource.verifyNoChanges()
|
||||||
|
@ -48,9 +49,9 @@ internal class RoomRefresherTest {
|
||||||
fun `given existing room when refreshing then processes existing state`() = runTest {
|
fun `given existing room when refreshing then processes existing state`() = runTest {
|
||||||
fakeRoomDataSource.expect { it.instance.persist(RoomId(any()), any(), any()) }
|
fakeRoomDataSource.expect { it.instance.persist(RoomId(any()), any(), any()) }
|
||||||
fakeRoomDataSource.givenRoom(A_ROOM_ID, ARoom.PREVIOUS_STATE)
|
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)
|
fakeRoomDataSource.verifyRoomUpdated(ARoom.PREVIOUS_STATE, ARoom.NEW_STATE)
|
||||||
result shouldBeEqualTo 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 A_MESSAGE_ROOM_EVENT = aRoomMessageEvent(anEventId("a-message"))
|
||||||
private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message"))
|
private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message"))
|
||||||
private val A_LOOKUP_EVENT_ID = anEventId("lookup-id")
|
private val A_LOOKUP_EVENT_ID = anEventId("lookup-id")
|
||||||
|
private val A_USER_CREDENTIALS = aUserCredentials()
|
||||||
|
|
||||||
class TimelineEventsProcessorTest {
|
class TimelineEventsProcessorTest {
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ class TimelineEventsProcessorTest {
|
||||||
fun `given a room with no events then returns empty`() = runTest {
|
fun `given a room with no events then returns empty`() = runTest {
|
||||||
val previousEvents = emptyList<RoomEvent>()
|
val previousEvents = emptyList<RoomEvent>()
|
||||||
val roomToProcess = aRoomToProcess()
|
val roomToProcess = aRoomToProcess()
|
||||||
fakeRoomEventsDecrypter.givenDecrypts(previousEvents)
|
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
|
||||||
fakeSyncEventDecrypter.givenDecrypts(roomToProcess.apiSyncRoom.timeline.apiTimelineEvents)
|
fakeSyncEventDecrypter.givenDecrypts(roomToProcess.apiSyncRoom.timeline.apiTimelineEvents)
|
||||||
|
|
||||||
val result = timelineEventsProcessor.process(roomToProcess, previousEvents)
|
val result = timelineEventsProcessor.process(roomToProcess, previousEvents)
|
||||||
|
@ -54,11 +55,18 @@ class TimelineEventsProcessorTest {
|
||||||
val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event")))
|
val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event")))
|
||||||
val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT)
|
val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT)
|
||||||
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
|
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
|
||||||
fakeRoomEventsDecrypter.givenDecrypts(previousEvents)
|
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
|
||||||
fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents)
|
fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents)
|
||||||
fakeEventLookup.givenLookup(A_LOOKUP_EVENT_ID, DecryptedTimeline(newTimelineEvents), DecryptedRoomEvents(previousEvents), ANY_LOOKUP_RESULT)
|
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.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)
|
val result = timelineEventsProcessor.process(roomToProcess, previousEvents)
|
||||||
|
|
||||||
|
@ -79,7 +87,7 @@ class TimelineEventsProcessorTest {
|
||||||
anIgnoredApiTimelineEvent()
|
anIgnoredApiTimelineEvent()
|
||||||
)
|
)
|
||||||
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
|
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
|
||||||
fakeRoomEventsDecrypter.givenDecrypts(previousEvents)
|
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
|
||||||
fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents)
|
fakeSyncEventDecrypter.givenDecrypts(newTimelineEvents)
|
||||||
|
|
||||||
val result = timelineEventsProcessor.process(roomToProcess, previousEvents)
|
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.EventId
|
||||||
import app.dapk.st.matrix.common.RoomId
|
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.RoomEvent
|
||||||
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
||||||
import app.dapk.st.matrix.sync.internal.sync.LookupResult
|
import app.dapk.st.matrix.sync.internal.sync.LookupResult
|
||||||
|
@ -18,9 +19,16 @@ internal class FakeRoomEventCreator {
|
||||||
coEvery { with(instance) { event.toRoomEvent(roomId) } } returns result
|
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>()
|
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 {
|
runBlocking {
|
||||||
if (slot.captured.invoke(eventIdToLookup) == lookupResult) {
|
if (slot.captured.invoke(eventIdToLookup) == lookupResult) {
|
||||||
result
|
result
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package internalfake
|
package internalfake
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.common.UserCredentials
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
|
import app.dapk.st.matrix.sync.internal.room.RoomEventsDecrypter
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
@ -8,7 +9,7 @@ import io.mockk.mockk
|
||||||
internal class FakeRoomEventsDecrypter {
|
internal class FakeRoomEventsDecrypter {
|
||||||
val instance = mockk<RoomEventsDecrypter>()
|
val instance = mockk<RoomEventsDecrypter>()
|
||||||
|
|
||||||
fun givenDecrypts(previousEvents: List<RoomEvent>, result: List<RoomEvent> = previousEvents) {
|
fun givenDecrypts(userCredentials: UserCredentials, previousEvents: List<RoomEvent>, result: List<RoomEvent> = previousEvents) {
|
||||||
coEvery { instance.decryptRoomEvents(previousEvents) } returns result
|
coEvery { instance.decryptRoomEvents(userCredentials, previousEvents) } returns result
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -39,9 +39,8 @@ internal fun anApiTimelineTextEvent(
|
||||||
internal fun aTimelineTextEventContent(
|
internal fun aTimelineTextEventContent(
|
||||||
body: String? = null,
|
body: String? = null,
|
||||||
formattedBody: String? = null,
|
formattedBody: String? = null,
|
||||||
type: String? = null,
|
|
||||||
relation: ApiTimelineEvent.TimelineMessage.Relation? = 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(
|
internal fun anEditRelation(originalId: EventId) = ApiTimelineEvent.TimelineMessage.Relation(
|
||||||
relationType = "m.replace",
|
relationType = "m.replace",
|
||||||
|
|
|
@ -15,8 +15,8 @@ fun aRoomMessageEvent(
|
||||||
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, encryptedContent, edited)
|
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, encryptedContent, edited)
|
||||||
|
|
||||||
fun aRoomReplyMessageEvent(
|
fun aRoomReplyMessageEvent(
|
||||||
message: RoomEvent.Message = aRoomMessageEvent(),
|
message: RoomEvent = aRoomMessageEvent(),
|
||||||
replyingTo: RoomEvent.Message = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")),
|
replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")),
|
||||||
) = RoomEvent.Reply(message, replyingTo)
|
) = RoomEvent.Reply(message, replyingTo)
|
||||||
|
|
||||||
fun anEncryptedRoomMessageEvent(
|
fun anEncryptedRoomMessageEvent(
|
||||||
|
|
Loading…
Reference in New Issue