diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index dd01094..acb967b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -1967,9 +1967,12 @@ fun ChatDetailScreen( onDismiss = { showMediaPicker = false }, isDarkTheme = isDarkTheme, onMediaSelected = { selectedMedia -> - // 📸 Отправляем выбранные изображения - android.util.Log.d("ChatDetailScreen", "📸 Sending ${selectedMedia.size} media items") + // 📸 Отправляем выбранные изображения как коллаж (группу) + android.util.Log.d("ChatDetailScreen", "📸 Sending ${selectedMedia.size} media items as group") scope.launch { + // Собираем все изображения + val imageDataList = mutableListOf() + for (item in selectedMedia) { if (item.isVideo) { // TODO: Поддержка видео @@ -1979,12 +1982,15 @@ fun ChatDetailScreen( val base64 = MediaUtils.uriToBase64Image(context, item.uri) val blurhash = MediaUtils.generateBlurhash(context, item.uri) if (base64 != null) { - viewModel.sendImageMessage(base64, blurhash) - // Небольшая задержка между отправками для правильного порядка - delay(100) + imageDataList.add(ChatViewModel.ImageData(base64, blurhash)) } } } + + // Отправляем группой (коллаж) + if (imageDataList.isNotEmpty()) { + viewModel.sendImageGroup(imageDataList) + } } }, onOpenCamera = { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 73bfc27..3c57a95 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -1391,17 +1391,30 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey) - // Шифруем изображение с ChaCha ключом + // 🚀 Шифруем изображение с ChaCha ключом для Transport Server val encryptedImageBlob = MessageCrypto.encryptReplyBlob(imageBase64, plainKeyAndNonce) - // Сохраняем оригинал для БД (зашифрованный приватным ключом) - val imageBlobForDatabase = CryptoManager.encryptWithPassword(imageBase64, privateKey) + val attachmentId = "img_$timestamp" + + // 📤 Загружаем на Transport Server (как в desktop) + // НЕ для Saved Messages - там не нужно загружать + val isSavedMessages = (sender == recipient) + var uploadTag = "" + + if (!isSavedMessages) { + Log.d(TAG, "📤 Uploading image to Transport Server...") + uploadTag = TransportManager.uploadFile(attachmentId, encryptedImageBlob) + Log.d(TAG, "📤 Upload complete, tag: $uploadTag") + } + + // Preview содержит tag::blurhash (как в desktop) + val previewWithTag = if (uploadTag.isNotEmpty()) "$uploadTag::$blurhash" else blurhash val imageAttachment = MessageAttachment( - id = "img_$timestamp", - blob = encryptedImageBlob, + id = attachmentId, + blob = "", // 🔥 Пустой blob - файл на Transport Server! type = AttachmentType.IMAGE, - preview = blurhash + preview = previewWithTag ) val packet = PacketMessage().apply { @@ -1415,8 +1428,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { attachments = listOf(imageAttachment) } - // Для Saved Messages не отправляем на сервер - val isSavedMessages = (sender == recipient) + // Отправляем пакет (без blob!) if (!isSavedMessages) { ProtocolManager.send(packet) } @@ -1425,12 +1437,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { updateMessageStatus(messageId, MessageStatus.SENT) } - // 💾 Сохраняем изображение в файл (как в desktop) - // Файлы НЕ сохраняем - они слишком большие, загружаются с CDN + // 💾 Сохраняем изображение в файл локально (как в desktop) AttachmentFileManager.saveAttachment( context = getApplication(), blob = imageBase64, - attachmentId = imageAttachment.id, + attachmentId = attachmentId, publicKey = sender, privateKey = privateKey ) @@ -1439,9 +1450,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { // Изображение хранится в файловой системе val attachmentsJson = JSONArray().apply { put(JSONObject().apply { - put("id", imageAttachment.id) + put("id", attachmentId) put("type", AttachmentType.IMAGE.value) - put("preview", blurhash) + put("preview", previewWithTag) put("blob", "") // Пустой blob - не сохраняем в БД! }) }.toString() @@ -1471,6 +1482,169 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } } + /** + * 🖼️ Отправка группы изображений как коллаж (как в Telegram) + * @param images Список пар (base64, blurhash) для каждого изображения + * @param caption Подпись к группе изображений (опционально) + */ + data class ImageData(val base64: String, val blurhash: String) + + fun sendImageGroup(images: List, caption: String = "") { + if (images.isEmpty()) return + + // Если одно изображение - отправляем обычным способом + if (images.size == 1) { + sendImageMessage(images[0].base64, images[0].blurhash, caption) + return + } + + val recipient = opponentKey + val sender = myPublicKey + val privateKey = myPrivateKey + + if (recipient == null || sender == null || privateKey == null) { + Log.e(TAG, "🖼️ Cannot send image group: missing keys") + return + } + if (isSending) { + Log.w(TAG, "🖼️ Already sending message") + return + } + + isSending = true + + val messageId = UUID.randomUUID().toString().replace("-", "").take(32) + val timestamp = System.currentTimeMillis() + val text = caption.trim() + + // Создаём attachments для всех изображений + val attachmentsList = images.mapIndexed { index, imageData -> + MessageAttachment( + id = "img_${timestamp}_$index", + type = AttachmentType.IMAGE, + preview = imageData.blurhash, + blob = imageData.base64 // Для локального отображения + ) + } + + // 1. 🚀 Optimistic UI + val optimisticMessage = ChatMessage( + id = messageId, + text = text, + isOutgoing = true, + timestamp = Date(timestamp), + status = MessageStatus.SENDING, + attachments = attachmentsList + ) + _messages.value = _messages.value + optimisticMessage + _inputText.value = "" + + Log.d(TAG, "🖼️ Sending image group: ${images.size} images, id=$messageId") + + viewModelScope.launch(Dispatchers.IO) { + try { + // Шифрование текста + val encryptResult = MessageCrypto.encryptForSending(text, recipient) + val encryptedContent = encryptResult.ciphertext + val encryptedKey = encryptResult.encryptedKey + val plainKeyAndNonce = encryptResult.plainKeyAndNonce + + val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey) + + // Загружаем каждое изображение на Transport Server и создаём attachments + val networkAttachments = mutableListOf() + val attachmentsJsonArray = JSONArray() + + for ((index, imageData) in images.withIndex()) { + val attachmentId = "img_${timestamp}_$index" + + // Шифруем изображение с ChaCha ключом + val encryptedImageBlob = MessageCrypto.encryptReplyBlob(imageData.base64, plainKeyAndNonce) + + // Загружаем на Transport Server + val uploadTag = TransportManager.uploadFile(attachmentId, encryptedImageBlob) + val previewWithTag = if (uploadTag != null) { + "$uploadTag::${imageData.blurhash}" + } else { + imageData.blurhash + } + + // Сохраняем в файл локально + AttachmentFileManager.saveAttachment( + context = getApplication(), + blob = imageData.base64, + attachmentId = attachmentId, + publicKey = sender, + privateKey = privateKey + ) + + // Для сети + networkAttachments.add(MessageAttachment( + id = attachmentId, + blob = if (uploadTag != null) "" else encryptedImageBlob, + type = AttachmentType.IMAGE, + preview = previewWithTag + )) + + // Для БД + attachmentsJsonArray.put(JSONObject().apply { + put("id", attachmentId) + put("type", AttachmentType.IMAGE.value) + put("preview", previewWithTag) + put("blob", "") // Пустой blob - изображения в файловой системе + }) + + Log.d(TAG, "🖼️ Image $index uploaded: tag=${uploadTag?.take(20) ?: "null"}") + } + + // Создаём пакет + val packet = PacketMessage().apply { + fromPublicKey = sender + toPublicKey = recipient + content = encryptedContent + chachaKey = encryptedKey + this.timestamp = timestamp + this.privateKey = privateKeyHash + this.messageId = messageId + attachments = networkAttachments + } + + // Для Saved Messages не отправляем на сервер + val isSavedMessages = (sender == recipient) + if (!isSavedMessages) { + ProtocolManager.send(packet) + } + + withContext(Dispatchers.Main) { + updateMessageStatus(messageId, MessageStatus.SENT) + } + + // Сохраняем в БД + saveMessageToDatabase( + messageId = messageId, + text = text, + encryptedContent = encryptedContent, + encryptedKey = encryptedKey, + timestamp = timestamp, + isFromMe = true, + delivered = if (isSavedMessages) 2 else 0, + attachmentsJson = attachmentsJsonArray.toString() + ) + + saveDialog(if (text.isNotEmpty()) text else "📷 ${images.size} photos", timestamp) + Log.d(TAG, "🖼️ ✅ Image group sent successfully") + + } catch (e: Exception) { + Log.e(TAG, "🖼️ ❌ Failed to send image group", e) + withContext(Dispatchers.Main) { + updateMessageStatus(messageId, MessageStatus.SENT) + } + } finally { + isSending = false + } + } + } + /** * 📄 Отправка сообщения с файлом * @param fileBase64 Base64 содержимого файла @@ -1529,15 +1703,30 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey) - // Шифруем файл + // 🚀 Шифруем файл с ChaCha ключом для Transport Server val encryptedFileBlob = MessageCrypto.encryptReplyBlob(fileBase64, plainKeyAndNonce) - val fileBlobForDatabase = CryptoManager.encryptWithPassword(fileBase64, privateKey) + + val attachmentId = "file_$timestamp" + + // 📤 Загружаем на Transport Server (как в desktop) + // НЕ для Saved Messages - там не нужно загружать + val isSavedMessages = (sender == recipient) + var uploadTag = "" + + if (!isSavedMessages) { + Log.d(TAG, "📤 Uploading file to Transport Server...") + uploadTag = TransportManager.uploadFile(attachmentId, encryptedFileBlob) + Log.d(TAG, "📤 Upload complete, tag: $uploadTag") + } + + // Preview содержит tag::size::name (как в desktop) + val previewWithTag = if (uploadTag.isNotEmpty()) "$uploadTag::$preview" else preview val fileAttachment = MessageAttachment( - id = "file_$timestamp", - blob = encryptedFileBlob, + id = attachmentId, + blob = "", // 🔥 Пустой blob - файл на Transport Server! type = AttachmentType.FILE, - preview = preview + preview = previewWithTag ) val packet = PacketMessage().apply { @@ -1551,7 +1740,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { attachments = listOf(fileAttachment) } - val isSavedMessages = (sender == recipient) + // Отправляем пакет (без blob!) if (!isSavedMessages) { ProtocolManager.send(packet) } @@ -1560,13 +1749,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { updateMessageStatus(messageId, MessageStatus.SENT) } - // ⚠️ НЕ сохраняем blob в БД - он слишком большой (SQLite CursorWindow 2MB limit) - // Файл должен храниться в файловой системе или загружаться с сервера при необходимости + // ⚠️ НЕ сохраняем файл локально - они слишком большие + // Файлы загружаются с Transport Server при необходимости val attachmentsJson = JSONArray().apply { put(JSONObject().apply { - put("id", fileAttachment.id) + put("id", attachmentId) put("type", AttachmentType.FILE.value) - put("preview", preview) + put("preview", previewWithTag) put("blob", "") // Пустой blob - не сохраняем в БД! }) }.toString() diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt index 12a920b..f4519a9 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt @@ -67,6 +67,7 @@ enum class DownloadStatus { /** * Composable для отображения всех attachments в сообщении + * 🖼️ IMAGE attachments группируются в коллаж (как в Telegram) */ @Composable fun MessageAttachments( @@ -83,23 +84,30 @@ fun MessageAttachments( ) { if (attachments.isEmpty()) return + // Разделяем attachments по типам + val imageAttachments = attachments.filter { it.type == AttachmentType.IMAGE } + val otherAttachments = attachments.filter { it.type != AttachmentType.IMAGE && it.type != AttachmentType.MESSAGES } + Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp) ) { - attachments.forEach { attachment -> + // 🖼️ Коллаж для изображений (если больше 1) + if (imageAttachments.isNotEmpty()) { + ImageCollage( + attachments = imageAttachments, + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus + ) + } + + // Остальные attachments по отдельности + otherAttachments.forEach { attachment -> when (attachment.type) { - AttachmentType.IMAGE -> { - ImageAttachment( - attachment = attachment, - chachaKey = chachaKey, - privateKey = privateKey, - isOutgoing = isOutgoing, - isDarkTheme = isDarkTheme, - timestamp = timestamp, - messageStatus = messageStatus - ) - } AttachmentType.FILE -> { FileAttachment( attachment = attachment, @@ -122,8 +130,273 @@ fun MessageAttachments( isDarkTheme = isDarkTheme ) } - AttachmentType.MESSAGES -> { - // MESSAGES обрабатываются отдельно как reply + else -> { /* MESSAGES обрабатываются отдельно */ } + } + } + } +} + +/** + * 🖼️ Коллаж изображений в стиле Telegram + * Разные layout'ы в зависимости от количества фото: + * - 1 фото: полная ширина + * - 2 фото: 2 колонки + * - 3 фото: 1 большое слева + 2 маленьких справа + * - 4 фото: 2x2 сетка + * - 5+ фото: 2 сверху + 3 снизу (или больше рядов) + */ +@Composable +fun ImageCollage( + attachments: List, + chachaKey: String, + privateKey: String, + isOutgoing: Boolean, + isDarkTheme: Boolean, + timestamp: java.util.Date, + messageStatus: MessageStatus = MessageStatus.READ, + modifier: Modifier = Modifier +) { + val count = attachments.size + val spacing = 2.dp + + // Показываем время и статус только на последнем изображении коллажа + val showOverlayOnLast = true + + Box( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + ) { + when (count) { + 1 -> { + // Одно фото - полная ширина + ImageAttachment( + attachment = attachments[0], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = true + ) + } + 2 -> { + // Два фото - горизонтально + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + attachments.forEachIndexed { index, attachment -> + Box(modifier = Modifier.weight(1f)) { + ImageAttachment( + attachment = attachment, + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = index == count - 1, + aspectRatio = 1f + ) + } + } + } + } + 3 -> { + // Три фото: 1 большое слева + 2 маленьких справа + Row( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + // Большое фото слева + Box(modifier = Modifier.weight(1.5f).fillMaxHeight()) { + ImageAttachment( + attachment = attachments[0], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = false, + fillMaxSize = true + ) + } + // Два маленьких справа + Column( + modifier = Modifier.weight(1f).fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(spacing) + ) { + Box(modifier = Modifier.weight(1f).fillMaxWidth()) { + ImageAttachment( + attachment = attachments[1], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = false, + fillMaxSize = true + ) + } + Box(modifier = Modifier.weight(1f).fillMaxWidth()) { + ImageAttachment( + attachment = attachments[2], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = true, + fillMaxSize = true + ) + } + } + } + } + 4 -> { + // Четыре фото: 2x2 сетка + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(spacing) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + Box(modifier = Modifier.weight(1f).aspectRatio(1f)) { + ImageAttachment( + attachment = attachments[0], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = false, + fillMaxSize = true + ) + } + Box(modifier = Modifier.weight(1f).aspectRatio(1f)) { + ImageAttachment( + attachment = attachments[1], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = false, + fillMaxSize = true + ) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + Box(modifier = Modifier.weight(1f).aspectRatio(1f)) { + ImageAttachment( + attachment = attachments[2], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = false, + fillMaxSize = true + ) + } + Box(modifier = Modifier.weight(1f).aspectRatio(1f)) { + ImageAttachment( + attachment = attachments[3], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = true, + fillMaxSize = true + ) + } + } + } + } + else -> { + // 5+ фото: 2 сверху + остальные снизу по 3 в ряд + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(spacing) + ) { + // Первые 2 фото сверху + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + Box(modifier = Modifier.weight(1f).aspectRatio(1f)) { + ImageAttachment( + attachment = attachments[0], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = false, + fillMaxSize = true + ) + } + Box(modifier = Modifier.weight(1f).aspectRatio(1f)) { + ImageAttachment( + attachment = attachments[1], + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = false, + fillMaxSize = true + ) + } + } + // Остальные по 3 в ряд + val remaining = attachments.drop(2) + remaining.chunked(3).forEachIndexed { rowIndex, rowItems -> + val isLastRow = rowIndex == remaining.chunked(3).size - 1 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + rowItems.forEachIndexed { index, attachment -> + val isLastItem = isLastRow && index == rowItems.size - 1 + Box(modifier = Modifier.weight(1f).aspectRatio(1f)) { + ImageAttachment( + attachment = attachment, + chachaKey = chachaKey, + privateKey = privateKey, + isOutgoing = isOutgoing, + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus, + showTimeOverlay = isLastItem, + fillMaxSize = true + ) + } + } + // Заполняем пустые места если в ряду меньше 3 фото + repeat(3 - rowItems.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } } } } @@ -141,7 +414,10 @@ fun ImageAttachment( isOutgoing: Boolean, isDarkTheme: Boolean, timestamp: java.util.Date, - messageStatus: MessageStatus = MessageStatus.READ + messageStatus: MessageStatus = MessageStatus.READ, + showTimeOverlay: Boolean = true, + aspectRatio: Float? = null, + fillMaxSize: Boolean = false ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -258,45 +534,56 @@ fun ImageAttachment( } // 📐 Вычисляем размер пузырька на основе соотношения сторон изображения (как в Telegram) - val (imageWidth, imageHeight) = remember(attachment.width, attachment.height) { - val maxWidth = 260.dp - val maxHeight = 340.dp - val minWidth = 180.dp - val minHeight = 140.dp - - if (attachment.width > 0 && attachment.height > 0) { - val aspectRatio = attachment.width.toFloat() / attachment.height.toFloat() - - when { - // Широкое изображение (landscape) - aspectRatio > 1.2f -> { - val width = maxWidth - val height = (maxWidth.value / aspectRatio).dp.coerceIn(minHeight, maxHeight) - width to height - } - // Высокое изображение (portrait) - aspectRatio < 0.8f -> { - val height = maxHeight - val width = (maxHeight.value * aspectRatio).dp.coerceIn(minWidth, maxWidth) - width to height - } - // Квадратное или близкое к квадрату - else -> { - val size = 220.dp - size to size - } - } + val (imageWidth, imageHeight) = remember(attachment.width, attachment.height, fillMaxSize, aspectRatio) { + // Если fillMaxSize - используем 100% контейнера + if (fillMaxSize) { + null to null } else { - // Fallback если размеры не указаны - 220.dp to 220.dp + val maxWidth = 260.dp + val maxHeight = 340.dp + val minWidth = 180.dp + val minHeight = 140.dp + + if (attachment.width > 0 && attachment.height > 0) { + val ar = attachment.width.toFloat() / attachment.height.toFloat() + + when { + // Широкое изображение (landscape) + ar > 1.2f -> { + val width = maxWidth + val height = (maxWidth.value / ar).dp.coerceIn(minHeight, maxHeight) + width to height + } + // Высокое изображение (portrait) + ar < 0.8f -> { + val height = maxHeight + val width = (maxHeight.value * ar).dp.coerceIn(minWidth, maxWidth) + width to height + } + // Квадратное или близкое к квадрату + else -> { + val size = 220.dp + size to size + } + } + } else { + // Fallback если размеры не указаны + 220.dp to 220.dp + } } } + // Модификатор размера + val sizeModifier = when { + fillMaxSize -> Modifier.fillMaxSize() + aspectRatio != null -> Modifier.fillMaxWidth().aspectRatio(aspectRatio) + imageWidth != null && imageHeight != null -> Modifier.width(imageWidth).height(imageHeight) + else -> Modifier.size(220.dp) + } + Box( - modifier = Modifier - .width(imageWidth) - .height(imageHeight) - .clip(RoundedCornerShape(12.dp)) + modifier = sizeModifier + .clip(RoundedCornerShape(if (fillMaxSize) 0.dp else 12.dp)) .background(Color.Transparent) .clickable { when (downloadStatus) { @@ -347,63 +634,65 @@ fun ImageAttachment( } } - // Время в правом нижнем углу (всегда показываем, даже когда фото blurred) - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(8.dp) - .background( - Color.Black.copy(alpha = 0.5f), - shape = RoundedCornerShape(10.dp) - ) - .padding(horizontal = 6.dp, vertical = 3.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp) + // Время в правом нижнем углу (показываем только если showTimeOverlay = true) + if (showTimeOverlay) { + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(8.dp) + .background( + Color.Black.copy(alpha = 0.5f), + shape = RoundedCornerShape(10.dp) + ) + .padding(horizontal = 6.dp, vertical = 3.dp) ) { - Text( - text = timeFormat.format(timestamp), - color = Color.White, - fontSize = 11.sp, - fontWeight = FontWeight.Medium - ) - if (isOutgoing) { - // Статус доставки для исходящих - when (messageStatus) { - MessageStatus.SENDING -> { - Icon( - compose.icons.TablerIcons.Clock, - contentDescription = null, - tint = Color.White.copy(alpha = 0.7f), - modifier = Modifier.size(14.dp) - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = timeFormat.format(timestamp), + color = Color.White, + fontSize = 11.sp, + fontWeight = FontWeight.Medium + ) + if (isOutgoing) { + // Статус доставки для исходящих + when (messageStatus) { + MessageStatus.SENDING -> { + Icon( + compose.icons.TablerIcons.Clock, + contentDescription = null, + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(14.dp) + ) + } + MessageStatus.SENT -> { + Icon( + compose.icons.TablerIcons.Check, + contentDescription = null, + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(14.dp) + ) + } + MessageStatus.DELIVERED -> { + Icon( + compose.icons.TablerIcons.Checks, + contentDescription = null, + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(14.dp) + ) + } + MessageStatus.READ -> { + Icon( + compose.icons.TablerIcons.Checks, + contentDescription = null, + tint = Color(0xFF4FC3F7), + modifier = Modifier.size(14.dp) + ) + } + else -> {} } - MessageStatus.SENT -> { - Icon( - compose.icons.TablerIcons.Check, - contentDescription = null, - tint = Color.White.copy(alpha = 0.7f), - modifier = Modifier.size(14.dp) - ) - } - MessageStatus.DELIVERED -> { - Icon( - compose.icons.TablerIcons.Checks, - contentDescription = null, - tint = Color.White.copy(alpha = 0.7f), - modifier = Modifier.size(14.dp) - ) - } - MessageStatus.READ -> { - Icon( - compose.icons.TablerIcons.Checks, - contentDescription = null, - tint = Color(0xFF4FC3F7), - modifier = Modifier.size(14.dp) - ) - } - else -> {} } } }