feat: Implement local file management for avatar attachments with download status checks
This commit is contained in:
@@ -1112,15 +1112,35 @@ fun AvatarAttachment(
|
|||||||
|
|
||||||
val timeFormat = remember { java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()) }
|
val timeFormat = remember { java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()) }
|
||||||
|
|
||||||
|
// Определяем начальный статус (как в Desktop calcDownloadStatus для AVATAR)
|
||||||
LaunchedEffect(attachment.id) {
|
LaunchedEffect(attachment.id) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
downloadStatus = when {
|
downloadStatus = when {
|
||||||
attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview) -> {
|
// 1. Если blob уже есть в памяти → DOWNLOADED
|
||||||
|
attachment.blob.isNotEmpty() -> {
|
||||||
|
Log.d(TAG, "📦 Avatar blob in memory for ${attachment.id}")
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
}
|
}
|
||||||
isDownloadTag(attachment.preview) -> {
|
// 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
|
DownloadStatus.NOT_DOWNLOADED
|
||||||
}
|
}
|
||||||
else -> DownloadStatus.DOWNLOADED
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode blurhash
|
// Decode blurhash
|
||||||
@@ -1134,9 +1154,27 @@ fun AvatarAttachment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadStatus == DownloadStatus.DOWNLOADED && attachment.blob.isNotEmpty()) {
|
// Загружаем аватар если статус DOWNLOADED
|
||||||
|
if (downloadStatus == DownloadStatus.DOWNLOADED) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
// 1. Сначала пробуем blob из памяти
|
||||||
|
if (attachment.blob.isNotEmpty()) {
|
||||||
|
Log.d(TAG, "🖼️ Loading avatar from blob")
|
||||||
avatarBitmap = base64ToBitmap(attachment.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) {
|
if (decrypted != null) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
avatarBitmap = base64ToBitmap(decrypted)
|
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)
|
avatarRepository?.saveAvatar(senderPublicKey, decrypted)
|
||||||
downloadStatus = DownloadStatus.DOWNLOADED
|
downloadStatus = DownloadStatus.DOWNLOADED
|
||||||
Log.d(TAG, "✅ Avatar downloaded and saved")
|
Log.d(TAG, "✅ Avatar downloaded and saved")
|
||||||
|
|||||||
@@ -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)
|
* Генерировать MD5 путь для аватара (совместимо с desktop)
|
||||||
* Desktop код:
|
* Desktop код:
|
||||||
|
|||||||
Reference in New Issue
Block a user