feat: Enhance avatar handling with improved database saving and display in chat UI
This commit is contained in:
@@ -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!")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user