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
|
privateKey = userPrivateKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. 💾 Сохраняем в БД (БЕЗ blob - он в файле)
|
// 4. 💾 Сохраняем в БД (БЕЗ blob - он в файле) - как в sendImageMessage
|
||||||
val attachmentJson = JSONObject().apply {
|
val attachmentsJson = JSONArray().apply {
|
||||||
put("id", avatarAttachmentId)
|
put(JSONObject().apply {
|
||||||
put("type", AttachmentType.AVATAR.value)
|
put("id", avatarAttachmentId)
|
||||||
put("preview", previewWithTag) // tag::blurhash
|
put("type", AttachmentType.AVATAR.value)
|
||||||
put("blob", "") // Пустой blob - не сохраняем в БД!
|
put("preview", previewWithTag) // tag::blurhash
|
||||||
}
|
put("blob", "") // Пустой blob - не сохраняем в БД!
|
||||||
|
})
|
||||||
|
}.toString()
|
||||||
|
|
||||||
messageDao.insertMessage(
|
saveMessageToDatabase(
|
||||||
MessageEntity(
|
messageId = messageId,
|
||||||
account = sender,
|
text = "", // Аватар без текста
|
||||||
fromPublicKey = sender,
|
encryptedContent = encryptedContent,
|
||||||
toPublicKey = recipient,
|
encryptedKey = encryptedKey,
|
||||||
content = encryptedContent,
|
timestamp = timestamp,
|
||||||
timestamp = timestamp,
|
isFromMe = true,
|
||||||
chachaKey = encryptedKey,
|
delivered = if (isSavedMessages) 2 else 0, // Как в sendImageMessage
|
||||||
read = 1,
|
attachmentsJson = attachmentsJson
|
||||||
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}"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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!")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,65 +1198,112 @@ 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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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) {
|
if (downloadStatus == DownloadStatus.ERROR) {
|
||||||
DownloadStatus.NOT_DOWNLOADED -> {
|
Icon(
|
||||||
Icon(
|
Icons.Default.Refresh,
|
||||||
Icons.Default.ArrowDownward,
|
contentDescription = "Retry",
|
||||||
contentDescription = "Download",
|
tint = Color(0xFFE53935),
|
||||||
tint = if (isOutgoing) Color.White else PrimaryBlue,
|
modifier = Modifier.size(24.dp)
|
||||||
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 -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user