From 659f0c097d32d10b663b126d67cc5b89abb658a8 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Wed, 4 Feb 2026 01:03:56 +0500 Subject: [PATCH] fix: fix image flashing --- .../chats/components/AttachmentComponents.kt | 89 +++++++++++++++++-- 1 file changed, 81 insertions(+), 8 deletions(-) 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 e13cc97..2e35f5b 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 @@ -4,6 +4,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import android.util.Base64 +import android.util.LruCache import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring @@ -55,6 +56,40 @@ import kotlin.math.min private const val TAG = "AttachmentComponents" +/** + * 🖼️ Глобальный LRU кэш для bitmap'ов изображений + * Это предотвращает мигание при перезаходе в диалог, + * так как изображения остаются в памяти между recomposition + */ +object ImageBitmapCache { + // Размер кэша = 1/8 доступной памяти (стандартная практика Android) + private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() + private val cacheSize = maxMemory / 8 + + private val cache = object : LruCache(cacheSize) { + override fun sizeOf(key: String, bitmap: Bitmap): Int { + // Размер в килобайтах + return bitmap.byteCount / 1024 + } + } + + fun get(key: String): Bitmap? = cache.get(key) + + fun put(key: String, bitmap: Bitmap) { + if (cache.get(key) == null) { + cache.put(key, bitmap) + } + } + + fun remove(key: String) { + cache.remove(key) + } + + fun clear() { + cache.evictAll() + } +} + /** * 📐 Telegram Bubble Specification * Все константы взяты из ChatMessageCell.java и Theme.java @@ -523,10 +558,21 @@ fun ImageAttachment( // Bounds для shared element transition var imageBounds by remember { mutableStateOf(null) } - var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) } - var imageBitmap by remember { mutableStateOf(null) } - var blurhashBitmap by remember { mutableStateOf(null) } - var downloadProgress by remember { mutableStateOf(0f) } + // 🔥 Ключ для глобального кэша + val cacheKey = "img_${attachment.id}" + + // 🔥 Сначала проверяем глобальный кэш + var downloadStatus by remember(attachment.id) { + mutableStateOf( + if (ImageBitmapCache.get(cacheKey) != null) DownloadStatus.DOWNLOADED + else DownloadStatus.PENDING + ) + } + var imageBitmap by remember(attachment.id) { + mutableStateOf(ImageBitmapCache.get(cacheKey)) + } + var blurhashBitmap by remember(attachment.id) { mutableStateOf(null) } + var downloadProgress by remember(attachment.id) { mutableStateOf(0f) } val preview = getPreview(attachment.preview) val downloadTag = getDownloadTag(attachment.preview) @@ -541,6 +587,19 @@ fun ImageAttachment( // Определяем начальный статус и декодируем blurhash (как в Desktop calcDownloadStatus) LaunchedEffect(attachment.id, attachment.localUri) { + // 🔥 Если изображение уже в глобальном кэше - используем его! + val cachedBitmap = ImageBitmapCache.get(cacheKey) + if (cachedBitmap != null) { + imageBitmap = cachedBitmap + downloadStatus = DownloadStatus.DOWNLOADED + return@LaunchedEffect + } + + // 🔥 Если изображение уже загружено локально - не перезагружаем! + if (imageBitmap != null && downloadStatus == DownloadStatus.DOWNLOADED) { + return@LaunchedEffect + } + // Определяем статус (логика из Desktop useAttachment.ts) withContext(Dispatchers.IO) { downloadStatus = @@ -594,7 +653,7 @@ fun ImageAttachment( if (downloadStatus == DownloadStatus.DOWNLOADED) { withContext(Dispatchers.IO) { // 🚀 0. Если есть localUri - загружаем напрямую из URI (optimistic UI) - if (attachment.localUri.isNotEmpty()) { + if (imageBitmap == null && attachment.localUri.isNotEmpty()) { try { val uri = android.net.Uri.parse(attachment.localUri) @@ -641,7 +700,11 @@ fun ImageAttachment( } } - imageBitmap = bitmap + if (bitmap != null) { + imageBitmap = bitmap + // 🔥 Сохраняем в глобальный кэш + ImageBitmapCache.put(cacheKey, bitmap) + } } catch (e: Exception) { // Fallback to other methods } @@ -649,7 +712,12 @@ fun ImageAttachment( // 1. Если нет bitmap, пробуем blob из памяти if (imageBitmap == null && attachment.blob.isNotEmpty()) { - imageBitmap = base64ToBitmap(attachment.blob) + val bitmap = base64ToBitmap(attachment.blob) + if (bitmap != null) { + imageBitmap = bitmap + // 🔥 Сохраняем в глобальный кэш + ImageBitmapCache.put(cacheKey, bitmap) + } } // 2. Если всё ещё нет, читаем из файловой системы @@ -662,7 +730,12 @@ fun ImageAttachment( privateKey ) if (localBlob != null) { - imageBitmap = base64ToBitmap(localBlob) + val bitmap = base64ToBitmap(localBlob) + if (bitmap != null) { + imageBitmap = bitmap + // 🔥 Сохраняем в глобальный кэш + ImageBitmapCache.put(cacheKey, bitmap) + } } else if (attachment.localUri.isEmpty()) { // Только если нет localUri - помечаем как NOT_DOWNLOADED downloadStatus = DownloadStatus.NOT_DOWNLOADED