From c2b7dab5f1d991d6bb6e2dbff7166b58b1bc986d Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 27 Jan 2026 18:46:47 +0500 Subject: [PATCH] feat: Enhance avatar handling with improved database saving and display in chat UI --- .../messenger/ui/chats/ChatViewModel.kt | 46 +++-- .../messenger/ui/chats/ChatsListScreen.kt | 1 + .../messenger/ui/chats/ChatsListViewModel.kt | 4 +- .../chats/components/AttachmentComponents.kt | 172 ++++++++++++------ 4 files changed, 146 insertions(+), 77 deletions(-) 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 8fd8166..21fe2a5 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 @@ -1995,34 +1995,30 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { privateKey = userPrivateKey ) - // 4. 💾 Сохраняем в БД (БЕЗ blob - он в файле) - val attachmentJson = JSONObject().apply { - put("id", avatarAttachmentId) - put("type", AttachmentType.AVATAR.value) - put("preview", previewWithTag) // tag::blurhash - put("blob", "") // Пустой blob - не сохраняем в БД! - } + // 4. 💾 Сохраняем в БД (БЕЗ blob - он в файле) - как в sendImageMessage + val attachmentsJson = JSONArray().apply { + put(JSONObject().apply { + put("id", avatarAttachmentId) + put("type", AttachmentType.AVATAR.value) + put("preview", previewWithTag) // tag::blurhash + put("blob", "") // Пустой blob - не сохраняем в БД! + }) + }.toString() - messageDao.insertMessage( - MessageEntity( - account = sender, - fromPublicKey = sender, - toPublicKey = recipient, - content = encryptedContent, - timestamp = timestamp, - chachaKey = encryptedKey, - read = 1, - fromMe = 1, - delivered = DeliveryStatus.DELIVERED.value, - messageId = messageId, - plainMessage = "", - attachments = JSONArray().apply { put(attachmentJson) }.toString(), - replyToMessageId = null, - dialogKey = if (sender < recipient) "${sender}_${recipient}" else "${recipient}_${sender}" - ) + saveMessageToDatabase( + messageId = messageId, + text = "", // Аватар без текста + encryptedContent = encryptedContent, + encryptedKey = encryptedKey, + timestamp = timestamp, + isFromMe = true, + delivered = if (isSavedMessages) 2 else 0, // Как в sendImageMessage + attachmentsJson = attachmentsJson ) - saveDialog("Photo", timestamp) + saveDialog("\$a=Avatar", timestamp) + + Log.d(TAG, "👤 💾 Avatar message saved to database: messageId=$messageId") Log.d(TAG, "👤 ✅ Avatar message sent successfully!") diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index 881f390..2061242 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -1904,6 +1904,7 @@ fun DialogItemContent( val displayText = when { dialog.lastMessageAttachmentType == "Photo" -> "Photo" dialog.lastMessageAttachmentType == "File" -> "File" + dialog.lastMessageAttachmentType == "Avatar" -> "Avatar" dialog.lastMessage.isEmpty() -> "No messages" else -> dialog.lastMessage } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt index 430931d..4e8dc05 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt @@ -148,7 +148,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio val actualDelivered = if (actualFromMe == 1) (lastMsgStatus?.delivered ?: 0) else 0 val actualRead = if (actualFromMe == 1) (lastMsgStatus?.read ?: 0) else 0 - // � Определяем тип attachment последнего сообщения + // 📎 Определяем тип attachment последнего сообщения val attachmentType = try { val attachmentsJson = messageDao.getLastMessageAttachments(publicKey, dialog.opponentKey) if (!attachmentsJson.isNullOrEmpty() && attachmentsJson != "[]") { @@ -159,6 +159,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio when (type) { 0 -> "Photo" // AttachmentType.IMAGE = 0 2 -> "File" // AttachmentType.FILE = 2 + 3 -> "Avatar" // AttachmentType.AVATAR = 3 else -> null } } else null @@ -242,6 +243,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio when (type) { 0 -> "Photo" // AttachmentType.IMAGE = 0 2 -> "File" // AttachmentType.FILE = 2 + 3 -> "Avatar" // AttachmentType.AVATAR = 3 else -> null } } else null 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 f4519a9..5196866 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 @@ -127,7 +127,9 @@ fun MessageAttachments( senderPublicKey = senderPublicKey, avatarRepository = avatarRepository, isOutgoing = isOutgoing, - isDarkTheme = isDarkTheme + isDarkTheme = isDarkTheme, + timestamp = timestamp, + messageStatus = messageStatus ) } else -> { /* MESSAGES обрабатываются отдельно */ } @@ -1028,7 +1030,9 @@ fun AvatarAttachment( senderPublicKey: String, avatarRepository: AvatarRepository?, isOutgoing: Boolean, - isDarkTheme: Boolean + isDarkTheme: Boolean, + timestamp: java.util.Date = java.util.Date(), + messageStatus: MessageStatus = MessageStatus.READ ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -1040,6 +1044,8 @@ fun AvatarAttachment( val preview = getPreview(attachment.preview) val downloadTag = getDownloadTag(attachment.preview) + val timeFormat = remember { java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()) } + LaunchedEffect(attachment.id) { downloadStatus = when { attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview) -> { @@ -1108,32 +1114,32 @@ fun AvatarAttachment( } } - // Telegram-style avatar attachment + // Telegram-style avatar attachment с временем и статусом Row( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(12.dp)) .background( if (isOutgoing) { - Color.White.copy(alpha = 0.15f) + Color.White.copy(alpha = 0.12f) } else { - if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF0F0F0) + if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5) } ) .clickable(enabled = downloadStatus == DownloadStatus.NOT_DOWNLOADED || downloadStatus == DownloadStatus.ERROR) { download() } - .padding(10.dp), + .padding(12.dp), verticalAlignment = Alignment.CenterVertically ) { - // Avatar preview + // Avatar preview - круглое изображение Box( modifier = Modifier - .size(48.dp) + .size(56.dp) .clip(CircleShape) .background( - if (isOutgoing) Color.White.copy(0.2f) - else (if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)) + if (isOutgoing) Color.White.copy(0.15f) + else (if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)) ), contentAlignment = Alignment.Center ) { @@ -1156,7 +1162,7 @@ fun AvatarAttachment( } downloadStatus == DownloadStatus.DOWNLOADING || downloadStatus == DownloadStatus.DECRYPTING -> { CircularProgressIndicator( - modifier = Modifier.size(20.dp), + modifier = Modifier.size(24.dp), color = if (isOutgoing) Color.White else PrimaryBlue, strokeWidth = 2.dp ) @@ -1165,9 +1171,26 @@ fun AvatarAttachment( Icon( Icons.Default.Person, contentDescription = null, - tint = if (isOutgoing) Color.White.copy(0.5f) - else (if (isDarkTheme) Color.White.copy(0.4f) else Color.Gray), - modifier = Modifier.size(26.dp) + tint = if (isOutgoing) Color.White.copy(0.6f) + else (if (isDarkTheme) Color.White.copy(0.5f) else Color.Gray), + modifier = Modifier.size(28.dp) + ) + } + } + + // Иконка скачивания поверх аватара если нужно скачать + if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.4f)), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.ArrowDownward, + contentDescription = "Download", + tint = Color.White, + modifier = Modifier.size(24.dp) ) } } @@ -1175,65 +1198,112 @@ fun AvatarAttachment( Spacer(modifier = Modifier.width(12.dp)) - // Info + // Info и время/статус Column(modifier = Modifier.weight(1f)) { + // Заголовок с иконкой замка Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "Avatar", - fontSize = 14.sp, - fontWeight = FontWeight.Medium, + text = "Profile Photo", + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, color = if (isOutgoing) Color.White else (if (isDarkTheme) Color.White else Color.Black) ) Spacer(modifier = Modifier.width(6.dp)) Icon( Icons.Default.Lock, - contentDescription = null, + contentDescription = "End-to-end encrypted", tint = if (isOutgoing) Color.White.copy(0.6f) else (if (isDarkTheme) Color.White.copy(0.4f) else Color.Gray), - modifier = Modifier.size(13.dp) + modifier = Modifier.size(12.dp) ) } + + Spacer(modifier = Modifier.height(2.dp)) + + // Описание статуса Text( text = when (downloadStatus) { DownloadStatus.DOWNLOADING -> "Downloading..." DownloadStatus.DECRYPTING -> "Decrypting..." - DownloadStatus.ERROR -> "Download failed" - DownloadStatus.DOWNLOADED -> "Profile photo shared" + DownloadStatus.ERROR -> "Tap to retry" + DownloadStatus.DOWNLOADED -> "Shared profile photo" else -> "Tap to download" }, - fontSize = 12.sp, + fontSize = 13.sp, color = if (isOutgoing) Color.White.copy(alpha = 0.7f) else (if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray) ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Время и статус доставки + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = timeFormat.format(timestamp), + fontSize = 11.sp, + color = if (isOutgoing) Color.White.copy(alpha = 0.6f) + else (if (isDarkTheme) Color.White.copy(alpha = 0.4f) else Color.Gray) + ) + + // Галочки статуса для исходящих сообщений + if (isOutgoing) { + when (messageStatus) { + MessageStatus.SENDING -> { + Icon( + compose.icons.TablerIcons.Clock, + contentDescription = "Sending", + tint = Color.White.copy(alpha = 0.6f), + modifier = Modifier.size(14.dp) + ) + } + MessageStatus.SENT -> { + Icon( + compose.icons.TablerIcons.Check, + contentDescription = "Sent", + tint = Color.White.copy(alpha = 0.6f), + modifier = Modifier.size(14.dp) + ) + } + MessageStatus.DELIVERED -> { + Icon( + compose.icons.TablerIcons.Checks, + contentDescription = "Delivered", + tint = Color.White.copy(alpha = 0.6f), + modifier = Modifier.size(14.dp) + ) + } + MessageStatus.READ -> { + Icon( + compose.icons.TablerIcons.Checks, + contentDescription = "Read", + tint = Color(0xFF4FC3F7), + modifier = Modifier.size(14.dp) + ) + } + MessageStatus.ERROR -> { + Icon( + Icons.Default.Error, + contentDescription = "Error", + tint = Color(0xFFE53935), + modifier = Modifier.size(14.dp) + ) + } + } + } + } } - // Download/status icon - when (downloadStatus) { - DownloadStatus.NOT_DOWNLOADED -> { - Icon( - Icons.Default.ArrowDownward, - contentDescription = "Download", - tint = if (isOutgoing) Color.White else PrimaryBlue, - modifier = Modifier.size(22.dp) - ) - } - DownloadStatus.ERROR -> { - Icon( - Icons.Default.Refresh, - contentDescription = "Retry", - tint = Color(0xFFE53935), - modifier = Modifier.size(22.dp) - ) - } - DownloadStatus.DOWNLOADED -> { - Icon( - Icons.Default.CheckCircle, - contentDescription = "Downloaded", - tint = Color(0xFF4CAF50), - modifier = Modifier.size(22.dp) - ) - } - else -> {} + // Иконка ошибки справа + if (downloadStatus == DownloadStatus.ERROR) { + Icon( + Icons.Default.Refresh, + contentDescription = "Retry", + tint = Color(0xFFE53935), + modifier = Modifier.size(24.dp) + ) } } }