Refactor and optimize various components

- Updated RosettaFirebaseMessagingService to use IO dispatcher for blocking calls.
- Enhanced AvatarRepository with LRU caching and improved coroutine handling for avatar loading.
- Implemented timeout for websocket connection in UnlockScreen.
- Added selection mode functionality in ChatsListScreen with haptic feedback and improved UI for chat actions.
- Improved animated dots in AttachmentComponents for a smoother visual effect.
- Refactored image downloading and caching logic in ChatDetailComponents to streamline the process.
- Optimized SwipeBackContainer to simplify gesture handling.
- Adjusted swipe back behavior in OtherProfileScreen based on image viewer state.
This commit is contained in:
2026-02-12 15:38:30 +05:00
parent 263d00b783
commit ea537ccce1
16 changed files with 775 additions and 1370 deletions

View File

@@ -5,8 +5,13 @@ import com.rosetta.messenger.database.AvatarCacheEntity
import com.rosetta.messenger.database.AvatarDao
import com.rosetta.messenger.utils.AvatarFileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import java.io.Closeable
/**
* Репозиторий для работы с аватарами
@@ -23,20 +28,31 @@ class AvatarRepository(
private val context: Context,
private val avatarDao: AvatarDao,
private val currentPublicKey: String
) {
) : Closeable {
companion object {
private const val TAG = "AvatarRepository"
private const val MAX_AVATAR_HISTORY = 5 // Хранить последние N аватаров
private const val MAX_CACHE_SIZE = 100
}
// Repository scope для coroutines
private val supervisorJob = kotlinx.coroutines.SupervisorJob()
private val repositoryScope = kotlinx.coroutines.CoroutineScope(
kotlinx.coroutines.SupervisorJob() + Dispatchers.IO
supervisorJob + Dispatchers.IO
)
// In-memory cache (как decodedAvatarsCache в desktop)
// publicKey -> Flow<List<AvatarInfo>>
private val memoryCache = mutableMapOf<String, MutableStateFlow<List<AvatarInfo>>>()
// In-memory LRU cache: publicKey -> (Flow, Job)
// При вытеснении отменяем Job подписки на БД
private data class CacheEntry(val flow: MutableStateFlow<List<AvatarInfo>>, val job: Job?)
private val memoryCache = object : LinkedHashMap<String, CacheEntry>(MAX_CACHE_SIZE, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, CacheEntry>?): Boolean {
if (size > MAX_CACHE_SIZE) {
eldest?.value?.job?.cancel()
return true
}
return false
}
}
/**
* Получить аватары пользователя
@@ -44,22 +60,21 @@ class AvatarRepository(
* @param allDecode true = вся история, false = только последний (для списков)
*/
fun getAvatars(publicKey: String, allDecode: Boolean = false): StateFlow<List<AvatarInfo>> {
// Проверяем memory cache
if (memoryCache.containsKey(publicKey)) {
return memoryCache[publicKey]!!.asStateFlow()
}
// Проверяем LRU cache (accessOrder=true обновляет позицию при get)
memoryCache[publicKey]?.let { return it.flow.asStateFlow() }
// Создаем новый flow для этого пользователя
val flow = MutableStateFlow<List<AvatarInfo>>(emptyList())
memoryCache[publicKey] = flow
// Подписываемся на изменения в БД с использованием repository scope
avatarDao.getAvatars(publicKey)
// Подписываемся на изменения в БД
val job = avatarDao.getAvatars(publicKey)
.onEach { entities ->
val avatars = if (allDecode) {
// Загружаем всю историю
entities.mapNotNull { entity ->
loadAndDecryptAvatar(entity)
// Параллельная загрузка всей истории
coroutineScope {
entities.map { entity -> async { loadAndDecryptAvatar(entity) } }
.awaitAll()
.filterNotNull()
}
} else {
// Загружаем только последний
@@ -70,7 +85,8 @@ class AvatarRepository(
flow.value = avatars
}
.launchIn(repositoryScope)
memoryCache[publicKey] = CacheEntry(flow, job)
return flow.asStateFlow()
}
@@ -107,13 +123,12 @@ class AvatarRepository(
avatarDao.deleteOldAvatars(fromPublicKey, MAX_AVATAR_HISTORY)
// 🔄 Обновляем memory cache если он существует
val cachedFlow = memoryCache[fromPublicKey]
if (cachedFlow != null) {
val cached = memoryCache[fromPublicKey]
if (cached != null) {
val avatarInfo = loadAndDecryptAvatar(entity)
if (avatarInfo != null) {
cachedFlow.value = listOf(avatarInfo)
cached.flow.value = listOf(avatarInfo)
}
} else {
}
} catch (e: Exception) {
@@ -172,8 +187,8 @@ class AvatarRepository(
// Удаляем из БД
avatarDao.deleteAllAvatars(currentPublicKey)
// Очищаем memory cache
memoryCache.remove(currentPublicKey)
// Очищаем memory cache + отменяем Job
memoryCache.remove(currentPublicKey)?.job?.cancel()
} catch (e: Exception) {
throw e
@@ -206,8 +221,14 @@ class AvatarRepository(
* Очистить memory cache (для освобождения памяти)
*/
fun clearMemoryCache() {
memoryCache.values.forEach { it.job?.cancel() }
memoryCache.clear()
}
override fun close() {
clearMemoryCache()
supervisorJob.cancel()
}
/**
* Предзагрузить системные аватары (для ботов/системных аккаунтов)
@@ -216,14 +237,14 @@ class AvatarRepository(
suspend fun preloadSystemAvatars(systemAccounts: Map<String, String>) {
withContext(Dispatchers.IO) {
systemAccounts.forEach { (publicKey, base64Avatar) ->
// Сохраняем только в memory cache, не в БД
// Сохраняем только в memory cache, не в БД (job=null — нет подписки)
val flow = MutableStateFlow(listOf(
AvatarInfo(
base64Data = base64Avatar,
timestamp = 0
)
))
memoryCache[publicKey] = flow
memoryCache[publicKey] = CacheEntry(flow, job = null)
}
}
}