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

This commit is contained in:
k1ngsterr1
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
)
// 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!")

View File

@@ -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
}

View File

@@ -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
// <EFBFBD> Определяем тип 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

View File

@@ -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)
)
}
}
}