diff --git a/android/core/data/src/main/java/io/picoclaw/android/core/data/local/ImageFileStorage.kt b/android/core/data/src/main/java/io/picoclaw/android/core/data/local/ImageFileStorage.kt index 9fcacfa5a..3d690db7f 100644 --- a/android/core/data/src/main/java/io/picoclaw/android/core/data/local/ImageFileStorage.kt +++ b/android/core/data/src/main/java/io/picoclaw/android/core/data/local/ImageFileStorage.kt @@ -1,7 +1,9 @@ package io.picoclaw.android.core.data.local import android.content.Context +import android.graphics.BitmapFactory import android.util.Base64 +import io.picoclaw.android.core.domain.model.ImageData import java.io.File import java.util.UUID @@ -9,10 +11,18 @@ class ImageFileStorage(context: Context) { private val imageDir = File(context.filesDir, "chat_images").also { it.mkdirs() } - fun saveBase64ToFile(base64: String): String { + fun saveBase64ToFile(base64: String): ImageData { val bytes = Base64.decode(base64, Base64.DEFAULT) val file = File(imageDir, "${UUID.randomUUID()}.jpg") file.writeBytes(bytes) - return file.absolutePath + + val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(file.absolutePath, opts) + + return ImageData( + path = file.absolutePath, + width = opts.outWidth, + height = opts.outHeight + ) } } diff --git a/android/core/data/src/main/java/io/picoclaw/android/core/data/mapper/MessageMapper.kt b/android/core/data/src/main/java/io/picoclaw/android/core/data/mapper/MessageMapper.kt index 508ed1873..9da512d70 100644 --- a/android/core/data/src/main/java/io/picoclaw/android/core/data/mapper/MessageMapper.kt +++ b/android/core/data/src/main/java/io/picoclaw/android/core/data/mapper/MessageMapper.kt @@ -5,18 +5,25 @@ import io.picoclaw.android.core.data.remote.dto.WsIncoming import io.picoclaw.android.core.data.remote.dto.WsOutgoing import io.picoclaw.android.core.domain.model.ChatMessage import io.picoclaw.android.core.domain.model.ImageAttachment +import io.picoclaw.android.core.domain.model.ImageData import io.picoclaw.android.core.domain.model.MessageSender import io.picoclaw.android.core.domain.model.MessageStatus +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.util.UUID +@Serializable +private data class ImageEntry(val path: String, val width: Int, val height: Int) + object MessageMapper { fun toDomain(entity: MessageEntity): ChatMessage { val images = entity.imagePathList?.let { try { - Json.decodeFromString>(it) + Json.decodeFromString>(it).map { e -> + ImageData(e.path, e.width, e.height) + } } catch (_: Exception) { emptyList() } @@ -43,9 +50,9 @@ object MessageMapper { ) } - fun toEntity(text: String, imagePaths: List, status: MessageStatus): MessageEntity { - val pathJson = if (imagePaths.isNotEmpty()) { - Json.encodeToString(imagePaths) + fun toEntity(text: String, images: List, status: MessageStatus): MessageEntity { + val pathJson = if (images.isNotEmpty()) { + Json.encodeToString(images.map { ImageEntry(it.path, it.width, it.height) }) } else null return MessageEntity( diff --git a/android/core/data/src/main/java/io/picoclaw/android/core/data/repository/ChatRepositoryImpl.kt b/android/core/data/src/main/java/io/picoclaw/android/core/data/repository/ChatRepositoryImpl.kt index cbc7088e2..ed517a6a4 100644 --- a/android/core/data/src/main/java/io/picoclaw/android/core/data/repository/ChatRepositoryImpl.kt +++ b/android/core/data/src/main/java/io/picoclaw/android/core/data/repository/ChatRepositoryImpl.kt @@ -48,8 +48,8 @@ class ChatRepositoryImpl( } override suspend fun sendMessage(text: String, images: List) { - val imagePaths = images.map { imageFileStorage.saveBase64ToFile(it.base64) } - val entity = MessageMapper.toEntity(text, imagePaths, MessageStatus.SENDING) + val imageDataList = images.map { imageFileStorage.saveBase64ToFile(it.base64) } + val entity = MessageMapper.toEntity(text, imageDataList, MessageStatus.SENDING) messageDao.insert(entity) val wsDto = MessageMapper.toWsIncoming(text, images) val success = webSocketClient.send(wsDto) diff --git a/android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ChatMessage.kt b/android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ChatMessage.kt index 81c30c4a5..719193cd8 100644 --- a/android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ChatMessage.kt +++ b/android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ChatMessage.kt @@ -4,7 +4,7 @@ data class ChatMessage( val id: String, val content: String, val sender: MessageSender, - val images: List = emptyList(), + val images: List = emptyList(), val timestamp: Long, val status: MessageStatus ) diff --git a/android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ImageData.kt b/android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ImageData.kt new file mode 100644 index 000000000..ab7abe70d --- /dev/null +++ b/android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ImageData.kt @@ -0,0 +1,7 @@ +package io.picoclaw.android.core.domain.model + +data class ImageData( + val path: String, + val width: Int, + val height: Int +) diff --git a/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/MessageBubble.kt b/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/MessageBubble.kt index c6b56c457..c3c02c533 100644 --- a/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/MessageBubble.kt +++ b/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/MessageBubble.kt @@ -2,6 +2,7 @@ package io.picoclaw.android.feature.chat.component import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn @@ -49,13 +50,18 @@ fun MessageBubble( modifier = Modifier.widthIn(max = 300.dp) ) { Column(modifier = Modifier.padding(12.dp)) { - message.images.forEach { filePath -> + message.images.forEach { imageData -> + val ratio = if (imageData.width > 0 && imageData.height > 0) { + imageData.width.toFloat() / imageData.height.toFloat() + } else 1f + AsyncImage( - model = File(filePath), + model = File(imageData.path), contentDescription = null, modifier = Modifier .fillMaxWidth() - .padding(bottom = 8.dp), + .padding(bottom = 8.dp) + .aspectRatio(ratio), contentScale = ContentScale.FillWidth ) }