feat: Enhance image attachment handling with collage display; support multiple layouts for images in messages

This commit is contained in:
k1ngsterr1
2026-01-26 18:07:07 +05:00
parent 0f652bea86
commit fe9bc50d32
3 changed files with 616 additions and 132 deletions

View File

@@ -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<ChatViewModel.ImageData>()
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 = {

View File

@@ -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<ImageData>, 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<MessageAttachment>()
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()

View File

@@ -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<MessageAttachment>,
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 -> {}
}
}
}