feat: Enhance avatar handling with improved database saving and display in chat UI

This commit is contained in:
2026-01-27 18:46:47 +05:00
parent 3247f2b284
commit c2b7dab5f1
4 changed files with 146 additions and 77 deletions

View File

@@ -1995,34 +1995,30 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
privateKey = userPrivateKey privateKey = userPrivateKey
) )
// 4. 💾 Сохраняем в БД (БЕЗ blob - он в файле) // 4. 💾 Сохраняем в БД (БЕЗ blob - он в файле) - как в sendImageMessage
val attachmentJson = JSONObject().apply { val attachmentsJson = JSONArray().apply {
put(JSONObject().apply {
put("id", avatarAttachmentId) put("id", avatarAttachmentId)
put("type", AttachmentType.AVATAR.value) put("type", AttachmentType.AVATAR.value)
put("preview", previewWithTag) // tag::blurhash put("preview", previewWithTag) // tag::blurhash
put("blob", "") // Пустой blob - не сохраняем в БД! put("blob", "") // Пустой blob - не сохраняем в БД!
} })
}.toString()
messageDao.insertMessage( saveMessageToDatabase(
MessageEntity(
account = sender,
fromPublicKey = sender,
toPublicKey = recipient,
content = encryptedContent,
timestamp = timestamp,
chachaKey = encryptedKey,
read = 1,
fromMe = 1,
delivered = DeliveryStatus.DELIVERED.value,
messageId = messageId, messageId = messageId,
plainMessage = "", text = "", // Аватар без текста
attachments = JSONArray().apply { put(attachmentJson) }.toString(), encryptedContent = encryptedContent,
replyToMessageId = null, encryptedKey = encryptedKey,
dialogKey = if (sender < recipient) "${sender}_${recipient}" else "${recipient}_${sender}" 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!") Log.d(TAG, "👤 ✅ Avatar message sent successfully!")

View File

@@ -1904,6 +1904,7 @@ fun DialogItemContent(
val displayText = when { val displayText = when {
dialog.lastMessageAttachmentType == "Photo" -> "Photo" dialog.lastMessageAttachmentType == "Photo" -> "Photo"
dialog.lastMessageAttachmentType == "File" -> "File" dialog.lastMessageAttachmentType == "File" -> "File"
dialog.lastMessageAttachmentType == "Avatar" -> "Avatar"
dialog.lastMessage.isEmpty() -> "No messages" dialog.lastMessage.isEmpty() -> "No messages"
else -> dialog.lastMessage else -> dialog.lastMessage
} }

View File

@@ -148,7 +148,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
val actualDelivered = if (actualFromMe == 1) (lastMsgStatus?.delivered ?: 0) else 0 val actualDelivered = if (actualFromMe == 1) (lastMsgStatus?.delivered ?: 0) else 0
val actualRead = if (actualFromMe == 1) (lastMsgStatus?.read ?: 0) else 0 val actualRead = if (actualFromMe == 1) (lastMsgStatus?.read ?: 0) else 0
// <EFBFBD> Определяем тип attachment последнего сообщения // 📎 Определяем тип attachment последнего сообщения
val attachmentType = try { val attachmentType = try {
val attachmentsJson = messageDao.getLastMessageAttachments(publicKey, dialog.opponentKey) val attachmentsJson = messageDao.getLastMessageAttachments(publicKey, dialog.opponentKey)
if (!attachmentsJson.isNullOrEmpty() && attachmentsJson != "[]") { if (!attachmentsJson.isNullOrEmpty() && attachmentsJson != "[]") {
@@ -159,6 +159,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
when (type) { when (type) {
0 -> "Photo" // AttachmentType.IMAGE = 0 0 -> "Photo" // AttachmentType.IMAGE = 0
2 -> "File" // AttachmentType.FILE = 2 2 -> "File" // AttachmentType.FILE = 2
3 -> "Avatar" // AttachmentType.AVATAR = 3
else -> null else -> null
} }
} else null } else null
@@ -242,6 +243,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
when (type) { when (type) {
0 -> "Photo" // AttachmentType.IMAGE = 0 0 -> "Photo" // AttachmentType.IMAGE = 0
2 -> "File" // AttachmentType.FILE = 2 2 -> "File" // AttachmentType.FILE = 2
3 -> "Avatar" // AttachmentType.AVATAR = 3
else -> null else -> null
} }
} else null } else null

View File

@@ -127,7 +127,9 @@ fun MessageAttachments(
senderPublicKey = senderPublicKey, senderPublicKey = senderPublicKey,
avatarRepository = avatarRepository, avatarRepository = avatarRepository,
isOutgoing = isOutgoing, isOutgoing = isOutgoing,
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme,
timestamp = timestamp,
messageStatus = messageStatus
) )
} }
else -> { /* MESSAGES обрабатываются отдельно */ } else -> { /* MESSAGES обрабатываются отдельно */ }
@@ -1028,7 +1030,9 @@ fun AvatarAttachment(
senderPublicKey: String, senderPublicKey: String,
avatarRepository: AvatarRepository?, avatarRepository: AvatarRepository?,
isOutgoing: Boolean, isOutgoing: Boolean,
isDarkTheme: Boolean isDarkTheme: Boolean,
timestamp: java.util.Date = java.util.Date(),
messageStatus: MessageStatus = MessageStatus.READ
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -1040,6 +1044,8 @@ fun AvatarAttachment(
val preview = getPreview(attachment.preview) val preview = getPreview(attachment.preview)
val downloadTag = getDownloadTag(attachment.preview) val downloadTag = getDownloadTag(attachment.preview)
val timeFormat = remember { java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()) }
LaunchedEffect(attachment.id) { LaunchedEffect(attachment.id) {
downloadStatus = when { downloadStatus = when {
attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview) -> { attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview) -> {
@@ -1108,32 +1114,32 @@ fun AvatarAttachment(
} }
} }
// Telegram-style avatar attachment // Telegram-style avatar attachment с временем и статусом
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(12.dp))
.background( .background(
if (isOutgoing) { if (isOutgoing) {
Color.White.copy(alpha = 0.15f) Color.White.copy(alpha = 0.12f)
} else { } else {
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF0F0F0) if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
} }
) )
.clickable(enabled = downloadStatus == DownloadStatus.NOT_DOWNLOADED || downloadStatus == DownloadStatus.ERROR) { .clickable(enabled = downloadStatus == DownloadStatus.NOT_DOWNLOADED || downloadStatus == DownloadStatus.ERROR) {
download() download()
} }
.padding(10.dp), .padding(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Avatar preview // Avatar preview - круглое изображение
Box( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(56.dp)
.clip(CircleShape) .clip(CircleShape)
.background( .background(
if (isOutgoing) Color.White.copy(0.2f) if (isOutgoing) Color.White.copy(0.15f)
else (if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)) else (if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8))
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -1156,7 +1162,7 @@ fun AvatarAttachment(
} }
downloadStatus == DownloadStatus.DOWNLOADING || downloadStatus == DownloadStatus.DECRYPTING -> { downloadStatus == DownloadStatus.DOWNLOADING || downloadStatus == DownloadStatus.DECRYPTING -> {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(20.dp), modifier = Modifier.size(24.dp),
color = if (isOutgoing) Color.White else PrimaryBlue, color = if (isOutgoing) Color.White else PrimaryBlue,
strokeWidth = 2.dp strokeWidth = 2.dp
) )
@@ -1165,9 +1171,26 @@ fun AvatarAttachment(
Icon( Icon(
Icons.Default.Person, Icons.Default.Person,
contentDescription = null, contentDescription = null,
tint = if (isOutgoing) Color.White.copy(0.5f) tint = if (isOutgoing) Color.White.copy(0.6f)
else (if (isDarkTheme) Color.White.copy(0.4f) else Color.Gray), else (if (isDarkTheme) Color.White.copy(0.5f) else Color.Gray),
modifier = Modifier.size(26.dp) 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,66 +1198,113 @@ fun AvatarAttachment(
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
// Info // Info и время/статус
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
// Заголовок с иконкой замка
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = "Avatar", text = "Profile Photo",
fontSize = 14.sp, fontSize = 15.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.SemiBold,
color = if (isOutgoing) Color.White else (if (isDarkTheme) Color.White else Color.Black) color = if (isOutgoing) Color.White else (if (isDarkTheme) Color.White else Color.Black)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Icon( Icon(
Icons.Default.Lock, Icons.Default.Lock,
contentDescription = null, contentDescription = "End-to-end encrypted",
tint = if (isOutgoing) Color.White.copy(0.6f) tint = if (isOutgoing) Color.White.copy(0.6f)
else (if (isDarkTheme) Color.White.copy(0.4f) else Color.Gray), 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(
text = when (downloadStatus) { text = when (downloadStatus) {
DownloadStatus.DOWNLOADING -> "Downloading..." DownloadStatus.DOWNLOADING -> "Downloading..."
DownloadStatus.DECRYPTING -> "Decrypting..." DownloadStatus.DECRYPTING -> "Decrypting..."
DownloadStatus.ERROR -> "Download failed" DownloadStatus.ERROR -> "Tap to retry"
DownloadStatus.DOWNLOADED -> "Profile photo shared" DownloadStatus.DOWNLOADED -> "Shared profile photo"
else -> "Tap to download" else -> "Tap to download"
}, },
fontSize = 12.sp, fontSize = 13.sp,
color = if (isOutgoing) Color.White.copy(alpha = 0.7f) color = if (isOutgoing) Color.White.copy(alpha = 0.7f)
else (if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray) else (if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray)
) )
}
// Download/status icon Spacer(modifier = Modifier.height(4.dp))
when (downloadStatus) {
DownloadStatus.NOT_DOWNLOADED -> { // Время и статус доставки
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( Icon(
Icons.Default.ArrowDownward, compose.icons.TablerIcons.Clock,
contentDescription = "Download", contentDescription = "Sending",
tint = if (isOutgoing) Color.White else PrimaryBlue, tint = Color.White.copy(alpha = 0.6f),
modifier = Modifier.size(22.dp) modifier = Modifier.size(14.dp)
) )
} }
DownloadStatus.ERROR -> { 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)
)
}
}
}
}
}
// Иконка ошибки справа
if (downloadStatus == DownloadStatus.ERROR) {
Icon( Icon(
Icons.Default.Refresh, Icons.Default.Refresh,
contentDescription = "Retry", contentDescription = "Retry",
tint = Color(0xFFE53935), tint = Color(0xFFE53935),
modifier = Modifier.size(22.dp) modifier = Modifier.size(24.dp)
) )
} }
DownloadStatus.DOWNLOADED -> {
Icon(
Icons.Default.CheckCircle,
contentDescription = "Downloaded",
tint = Color(0xFF4CAF50),
modifier = Modifier.size(22.dp)
)
}
else -> {}
}
} }
} }