rendering images in the messenger
This commit is contained in:
parent
5a6b7cf1c0
commit
d0b2627eb2
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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.*
|
||||||
|
@ -179,16 +180,9 @@ private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPrevi
|
||||||
isNotSelf = false,
|
isNotSelf = false,
|
||||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
||||||
) {
|
) {
|
||||||
Text(message.imageMeta.url)
|
Box {
|
||||||
androidx.compose.foundation.Image(
|
MessageImage(message)
|
||||||
painter = rememberImagePainter(
|
}
|
||||||
data = message.imageMeta.url,
|
|
||||||
),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(100.dp)
|
|
||||||
.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,25 +194,29 @@ private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPrevi
|
||||||
isNotSelf = true,
|
isNotSelf = true,
|
||||||
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
wasPreviousMessageSameSender = wasPreviousMessageSameSender
|
||||||
) {
|
) {
|
||||||
Text(message.imageMeta.url)
|
Box {
|
||||||
androidx.compose.foundation.Image(
|
MessageImage(message)
|
||||||
painter = rememberImagePainter(
|
|
||||||
data = message.imageMeta.url,
|
|
||||||
builder = {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(100.dp)
|
|
||||||
.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageImage(message: RoomEvent.Image) {
|
||||||
|
val width = with(LocalDensity.current) { message.imageMeta.width.toDp() }
|
||||||
|
val height = with(LocalDensity.current) { message.imageMeta.height.toDp() }
|
||||||
|
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(width, height),
|
||||||
|
painter = rememberImagePainter(
|
||||||
|
data = message,
|
||||||
|
builder = { fetcher(DecryptingFetcher()) }
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) {
|
private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) {
|
||||||
|
|
|
@ -93,7 +93,18 @@ sealed class RoomEvent {
|
||||||
@SerialName("width") val width: Int,
|
@SerialName("width") val width: Int,
|
||||||
@SerialName("height") val height: Int,
|
@SerialName("height") val height: Int,
|
||||||
@SerialName("url") val url: String,
|
@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>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -459,7 +459,8 @@ internal sealed class ApiTimelineEvent {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Image(
|
data class Image(
|
||||||
@SerialName("file") val file: File,
|
@SerialName("url") val url: MxUrl? = null,
|
||||||
|
@SerialName("file") val file: File? = null,
|
||||||
@SerialName("info") val info: Info,
|
@SerialName("info") val info: Info,
|
||||||
@SerialName("m.relates_to") override val relation: Relation? = null,
|
@SerialName("m.relates_to") override val relation: Relation? = null,
|
||||||
@SerialName("msgtype") val messageType: String = "m.image",
|
@SerialName("msgtype") val messageType: String = "m.image",
|
||||||
|
@ -468,14 +469,24 @@ internal sealed class ApiTimelineEvent {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class File(
|
data class File(
|
||||||
@SerialName("url") val url: MxUrl,
|
@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
|
@Serializable
|
||||||
internal data class Info(
|
internal data class Info(
|
||||||
@SerialName("h") val height: Int,
|
@SerialName("h") val height: Int,
|
||||||
@SerialName("w") val width: Int,
|
@SerialName("w") val width: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -553,10 +564,7 @@ internal object ApiTimelineMessageContentDeserializer : KSerializer<ApiTimelineE
|
||||||
val element = decoder.decodeJsonElement()
|
val element = decoder.decodeJsonElement()
|
||||||
return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) {
|
return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) {
|
||||||
"m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder)
|
"m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder)
|
||||||
"m.image" -> when (element.jsonObject["file"]) {
|
"m.image" -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
|
||||||
null -> ApiTimelineEvent.TimelineMessage.Content.Ignored
|
|
||||||
else -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
|
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
println(element)
|
println(element)
|
||||||
ApiTimelineEvent.TimelineMessage.Content.Ignored
|
ApiTimelineEvent.TimelineMessage.Content.Ignored
|
||||||
|
|
|
@ -37,7 +37,8 @@ internal class RoomEventsDecrypter(
|
||||||
imageMeta = RoomEvent.Image.ImageMeta(
|
imageMeta = RoomEvent.Image.ImageMeta(
|
||||||
width = content.info.width,
|
width = content.info.width,
|
||||||
height = content.info.height,
|
height = content.info.height,
|
||||||
url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: ""
|
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,
|
encryptedContent = null,
|
||||||
)
|
)
|
||||||
|
|
|
@ -47,7 +47,8 @@ internal class RoomEventFactory(
|
||||||
return RoomEvent.Image.ImageMeta(
|
return RoomEvent.Image.ImageMeta(
|
||||||
content.info.width,
|
content.info.width,
|
||||||
content.info.height,
|
content.info.height,
|
||||||
content.file?.url?.convertMxUrToUrl(userCredentials.homeServer)
|
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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue