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 a1002c2..ba49f52 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 @@ -1112,15 +1112,35 @@ fun AvatarAttachment( val timeFormat = remember { java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()) } + // Определяем начальный статус (как в Desktop calcDownloadStatus для AVATAR) LaunchedEffect(attachment.id) { - downloadStatus = when { - attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview) -> { - DownloadStatus.DOWNLOADED + withContext(Dispatchers.IO) { + downloadStatus = when { + // 1. Если blob уже есть в памяти → DOWNLOADED + attachment.blob.isNotEmpty() -> { + Log.d(TAG, "📦 Avatar blob in memory for ${attachment.id}") + DownloadStatus.DOWNLOADED + } + // 2. Если preview НЕ содержит UUID → локальный файл → DOWNLOADED + !isDownloadTag(attachment.preview) -> { + Log.d(TAG, "📦 No download tag for avatar ${attachment.id}") + DownloadStatus.DOWNLOADED + } + // 3. Есть UUID (download tag) → проверяем файловую систему + // Desktop: readFile(`a/${md5(attachment.id + publicKey)}`) + else -> { + val hasLocal = AvatarFileManager.hasAvatarByAttachmentId( + context, attachment.id, senderPublicKey + ) + if (hasLocal) { + Log.d(TAG, "📦 Found local avatar file for ${attachment.id}") + DownloadStatus.DOWNLOADED + } else { + Log.d(TAG, "📥 Need to download avatar ${attachment.id}") + DownloadStatus.NOT_DOWNLOADED + } + } } - isDownloadTag(attachment.preview) -> { - DownloadStatus.NOT_DOWNLOADED - } - else -> DownloadStatus.DOWNLOADED } // Decode blurhash @@ -1134,9 +1154,27 @@ fun AvatarAttachment( } } - if (downloadStatus == DownloadStatus.DOWNLOADED && attachment.blob.isNotEmpty()) { + // Загружаем аватар если статус DOWNLOADED + if (downloadStatus == DownloadStatus.DOWNLOADED) { withContext(Dispatchers.IO) { - avatarBitmap = base64ToBitmap(attachment.blob) + // 1. Сначала пробуем blob из памяти + if (attachment.blob.isNotEmpty()) { + Log.d(TAG, "🖼️ Loading avatar from blob") + avatarBitmap = base64ToBitmap(attachment.blob) + } else { + // 2. Читаем из файловой системы (как в Desktop getBlob) + Log.d(TAG, "🖼️ Loading avatar from local file") + val localBlob = AvatarFileManager.readAvatarByAttachmentId( + context, attachment.id, senderPublicKey + ) + if (localBlob != null) { + avatarBitmap = base64ToBitmap(localBlob) + Log.d(TAG, "✅ Avatar loaded from local file") + } else { + Log.w(TAG, "⚠️ Failed to read local avatar file") + downloadStatus = DownloadStatus.NOT_DOWNLOADED + } + } } } } @@ -1164,8 +1202,18 @@ fun AvatarAttachment( if (decrypted != null) { withContext(Dispatchers.IO) { avatarBitmap = base64ToBitmap(decrypted) + + // 💾 Сохраняем в файловую систему по attachment.id (как в Desktop) + // Desktop: writeFile(`a/${md5(attachment.id + publicKey)}`, encrypted) + AvatarFileManager.saveAvatarByAttachmentId( + context = context, + base64Image = decrypted, + attachmentId = attachment.id, + publicKey = senderPublicKey + ) + Log.d(TAG, "💾 Avatar saved to local file system") } - // Сохраняем аватар в репозиторий + // Сохраняем аватар в репозиторий (для UI обновления) avatarRepository?.saveAvatar(senderPublicKey, decrypted) downloadStatus = DownloadStatus.DOWNLOADED Log.d(TAG, "✅ Avatar downloaded and saved") diff --git a/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt b/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt index 3e38553..b202217 100644 --- a/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt +++ b/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt @@ -107,6 +107,79 @@ object AvatarFileManager { } } + /** + * Проверить существует ли аватар по attachment ID (как в Desktop) + * Desktop: readFile(`a/${md5(attachment.id + publicKey)}`) + * + * @param context Android context + * @param attachmentId ID attachment + * @param publicKey Публичный ключ пользователя + * @return true если файл существует + */ + fun hasAvatarByAttachmentId(context: Context, attachmentId: String, publicKey: String): Boolean { + val path = generatePathByAttachmentId(attachmentId, publicKey) + val dir = File(context.filesDir, AVATAR_DIR) + val file = File(dir, path) + return file.exists() + } + + /** + * Прочитать аватар по attachment ID (как в Desktop) + * Desktop: readFile(`a/${md5(attachment.id + publicKey)}`) + * + * @param context Android context + * @param attachmentId ID attachment + * @param publicKey Публичный ключ пользователя + * @return Base64-encoded изображение или null + */ + fun readAvatarByAttachmentId(context: Context, attachmentId: String, publicKey: String): String? { + val path = generatePathByAttachmentId(attachmentId, publicKey) + return readAvatar(context, path) + } + + /** + * Сохранить аватар по attachment ID (как в Desktop) + * Desktop: writeFile(`a/${md5(attachment.id + publicKey)}`, encrypted) + * + * @param context Android context + * @param base64Image Base64-encoded изображение + * @param attachmentId ID attachment + * @param publicKey Публичный ключ пользователя + * @return Путь к файлу + */ + fun saveAvatarByAttachmentId(context: Context, base64Image: String, attachmentId: String, publicKey: String): String { + val path = generatePathByAttachmentId(attachmentId, publicKey) + + // Шифруем данные с паролем "rosetta-a" + val encrypted = CryptoManager.encryptWithPassword(base64Image, AVATAR_PASSWORD) + + // Сохраняем в файловую систему + val dir = File(context.filesDir, AVATAR_DIR) + dir.mkdirs() + + val parts = path.split("/") + if (parts.size == 2) { + val subDir = File(dir, parts[0]) + subDir.mkdirs() + val file = File(subDir, parts[1]) + file.writeText(encrypted) + android.util.Log.d("AvatarFileManager", "💾 Avatar saved to: ${file.absolutePath}") + } + + return path + } + + /** + * Генерировать путь по attachment ID (как в Desktop) + * Desktop: `a/${md5(attachment.id + publicKey)}` + */ + private fun generatePathByAttachmentId(attachmentId: String, publicKey: String): String { + val md5 = MessageDigest.getInstance("MD5") + .digest("$attachmentId$publicKey".toByteArray()) + .joinToString("") { "%02x".format(it) } + return "a/$md5" + } + /** * Генерировать MD5 путь для аватара (совместимо с desktop) * Desktop код: