feat: Integrate AvatarRepository into chat components for improved avatar handling and caching

This commit is contained in:
2026-01-28 00:28:56 +05:00
parent f92bc8b0d5
commit 8702539d09
8 changed files with 142 additions and 73 deletions

View File

@@ -717,7 +717,8 @@ fun MainScreen(
// 📨 Forward: переход в выбранный чат с полными данными
selectedUser = forwardUser
},
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
avatarRepository = avatarRepository
)
}
}
@@ -841,7 +842,8 @@ fun MainScreen(
onBack = {
showOtherProfileScreen = false
selectedOtherUser = null
}
},
avatarRepository = avatarRepository
)
}
}

View File

@@ -30,6 +30,11 @@ class AvatarRepository(
private const val MAX_AVATAR_HISTORY = 5 // Хранить последние N аватаров
}
// Repository scope для coroutines
private val repositoryScope = kotlinx.coroutines.CoroutineScope(
kotlinx.coroutines.SupervisorJob() + Dispatchers.IO
)
// In-memory cache (как decodedAvatarsCache в desktop)
// publicKey -> Flow<List<AvatarInfo>>
private val memoryCache = mutableMapOf<String, MutableStateFlow<List<AvatarInfo>>>()
@@ -49,9 +54,10 @@ class AvatarRepository(
val flow = MutableStateFlow<List<AvatarInfo>>(emptyList())
memoryCache[publicKey] = flow
// Подписываемся на изменения в БД
// Подписываемся на изменения в БД с использованием repository scope
avatarDao.getAvatars(publicKey)
.onEach { entities ->
Log.d(TAG, "📥 DB update for $publicKey: ${entities.size} entities")
val avatars = if (allDecode) {
// Загружаем всю историю
entities.mapNotNull { entity ->
@@ -64,8 +70,9 @@ class AvatarRepository(
}?.let { listOf(it) } ?: emptyList()
}
flow.value = avatars
Log.d(TAG, "✅ Flow updated for $publicKey: ${avatars.size} avatars")
}
.launchIn(kotlinx.coroutines.CoroutineScope(Dispatchers.IO))
.launchIn(repositoryScope)
return flow.asStateFlow()
}
@@ -90,6 +97,7 @@ class AvatarRepository(
try {
// Сохраняем файл
val filePath = AvatarFileManager.saveAvatar(context, base64Image, fromPublicKey)
Log.d(TAG, "💾 Avatar file saved for $fromPublicKey: $filePath")
// Сохраняем в БД
val entity = AvatarCacheEntity(
@@ -98,13 +106,26 @@ class AvatarRepository(
timestamp = System.currentTimeMillis()
)
avatarDao.insertAvatar(entity)
Log.d(TAG, "💾 Avatar entity inserted to DB for $fromPublicKey")
// Очищаем старые аватары (оставляем только последние N)
avatarDao.deleteOldAvatars(fromPublicKey, MAX_AVATAR_HISTORY)
Log.d(TAG, "Saved avatar for $fromPublicKey")
// 🔄 Обновляем memory cache если он существует
val cachedFlow = memoryCache[fromPublicKey]
if (cachedFlow != null) {
val avatarInfo = loadAndDecryptAvatar(entity)
if (avatarInfo != null) {
cachedFlow.value = listOf(avatarInfo)
Log.d(TAG, "✅ Memory cache updated for $fromPublicKey")
}
} else {
Log.d(TAG, " No memory cache for $fromPublicKey, will be loaded on next getAvatars()")
}
Log.d(TAG, "✅ Saved avatar for $fromPublicKey")
} catch (e: Exception) {
Log.e(TAG, "Failed to save avatar", e)
Log.e(TAG, "Failed to save avatar for $fromPublicKey", e)
}
}
}

View File

@@ -59,6 +59,8 @@ import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.rosetta.messenger.R
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.chats.models.*
import com.rosetta.messenger.ui.chats.utils.*
import com.rosetta.messenger.ui.chats.components.*
@@ -97,7 +99,8 @@ fun ChatDetailScreen(
currentUserPublicKey: String,
currentUserPrivateKey: String,
totalUnreadFromOthers: Int = 0,
isDarkTheme: Boolean
isDarkTheme: Boolean,
avatarRepository: AvatarRepository? = null
) {
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")
val context = LocalContext.current
@@ -780,17 +783,6 @@ fun ChatDetailScreen(
Box(
modifier =
Modifier.size(40.dp)
.clip(
CircleShape
)
.background(
if (isSavedMessages
)
PrimaryBlue
else
avatarColors
.backgroundColor
)
.clickable(
indication =
null,
@@ -809,41 +801,32 @@ fun ChatDetailScreen(
Alignment.Center
) {
if (isSavedMessages) {
Icon(
Icons.Default
.Bookmark,
contentDescription =
null,
tint =
Color.White,
modifier =
Modifier.size(
20.dp
)
)
} else {
Text(
text =
if (user.title
.isNotEmpty()
)
getInitials(
user.title
Box(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default
.Bookmark,
contentDescription =
null,
tint =
Color.White,
modifier =
Modifier.size(
20.dp
)
else
user.publicKey
.take(
2
)
.uppercase(),
fontSize =
14.sp,
fontWeight =
FontWeight
.Bold,
color =
avatarColors
.textColor
)
}
} else {
AvatarImage(
publicKey = user.publicKey,
avatarRepository = avatarRepository,
size = 40.dp,
isDarkTheme = isDarkTheme
)
}
}
@@ -1671,6 +1654,10 @@ fun ChatDetailScreen(
currentUserPrivateKey,
senderPublicKey =
if (message.isOutgoing) currentUserPublicKey else user.publicKey,
currentUserPublicKey =
currentUserPublicKey,
avatarRepository =
avatarRepository,
onLongClick = {
if (!isSelectionMode
) {

View File

@@ -81,6 +81,7 @@ fun MessageAttachments(
timestamp: java.util.Date,
messageStatus: MessageStatus = MessageStatus.READ,
avatarRepository: AvatarRepository? = null,
currentUserPublicKey: String = "",
modifier: Modifier = Modifier
) {
if (attachments.isEmpty()) return
@@ -128,6 +129,7 @@ fun MessageAttachments(
privateKey = privateKey,
senderPublicKey = senderPublicKey,
avatarRepository = avatarRepository,
currentUserPublicKey = currentUserPublicKey,
isOutgoing = isOutgoing,
isDarkTheme = isDarkTheme,
timestamp = timestamp,
@@ -1123,6 +1125,7 @@ fun AvatarAttachment(
privateKey: String,
senderPublicKey: String,
avatarRepository: AvatarRepository?,
currentUserPublicKey: String = "",
isOutgoing: Boolean,
isDarkTheme: Boolean,
timestamp: java.util.Date = java.util.Date(),
@@ -1263,8 +1266,28 @@ fun AvatarAttachment(
Log.d(TAG, "💾 Avatar saved to: $path")
}
// Сохраняем аватар в репозиторий (для UI обновления)
Log.d(TAG, "💾 Saving avatar to repository for user ${senderPublicKey.take(16)}...")
avatarRepository?.saveAvatar(senderPublicKey, decrypted)
// Если это исходящее сообщение с аватаром, сохраняем для текущего пользователя
val targetPublicKey = if (isOutgoing && currentUserPublicKey.isNotEmpty()) {
Log.d(TAG, "💾 Saving avatar to repository for CURRENT user ${currentUserPublicKey.take(16)}...")
currentUserPublicKey
} else {
Log.d(TAG, "💾 Saving avatar to repository for SENDER ${senderPublicKey.take(16)}...")
senderPublicKey
}
// ВАЖНО: ждем завершения сохранения в репозиторий
if (avatarRepository != null) {
try {
Log.d(TAG, "📤 Calling avatarRepository.saveAvatar()...")
avatarRepository.saveAvatar(targetPublicKey, decrypted)
Log.d(TAG, "✅ Avatar saved to repository for ${targetPublicKey.take(16)}")
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to save avatar to repository: ${e.message}", e)
}
} else {
Log.e(TAG, "❌ avatarRepository is NULL! Cannot save avatar for ${targetPublicKey.take(16)}")
}
downloadStatus = DownloadStatus.DOWNLOADED
Log.d(TAG, "=====================================")
Log.d(TAG, "✅ AVATAR DOWNLOAD COMPLETE")

View File

@@ -41,6 +41,7 @@ import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import com.rosetta.messenger.network.AttachmentType
import com.rosetta.messenger.network.MessageAttachment
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.components.AppleEmojiText
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.chats.models.*
@@ -140,6 +141,8 @@ fun MessageBubble(
isSavedMessages: Boolean = false,
privateKey: String = "",
senderPublicKey: String = "",
currentUserPublicKey: String = "",
avatarRepository: AvatarRepository? = null,
onLongClick: () -> Unit = {},
onClick: () -> Unit = {},
onSwipeToReply: () -> Unit = {},
@@ -380,7 +383,9 @@ fun MessageBubble(
isDarkTheme = isDarkTheme,
senderPublicKey = senderPublicKey,
timestamp = message.timestamp,
messageStatus = message.status
messageStatus = message.status,
avatarRepository = avatarRepository,
currentUserPublicKey = currentUserPublicKey
)
if (message.text.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))

View File

@@ -70,11 +70,14 @@ fun AvatarImage(
// Декодируем первый аватар
LaunchedEffect(avatars) {
bitmap = if (avatars.isNotEmpty()) {
android.util.Log.d("AvatarImage", "🔄 Decoding avatar for ${publicKey.take(16)}...")
android.util.Log.d("AvatarImage", "🔄 Decoding avatar for ${publicKey.take(16)}... base64 length=${avatars.first().base64Data?.length ?: 0}")
withContext(Dispatchers.IO) {
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
val result = AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
android.util.Log.d("AvatarImage", "✅ Decoded bitmap for ${publicKey.take(16)}... result=${result != null} size=${result?.width}x${result?.height}")
result
}
} else {
android.util.Log.d("AvatarImage", "⚠️ No avatars for ${publicKey.take(16)}...")
null
}
}
@@ -92,6 +95,11 @@ fun AvatarImage(
),
contentAlignment = Alignment.Center
) {
// Log what we're showing
LaunchedEffect(bitmap) {
android.util.Log.d("AvatarImage", "🖼️ Showing for ${publicKey.take(16)}... bitmap=${bitmap != null}")
}
if (bitmap != null) {
// Отображаем реальный аватар
Image(

View File

@@ -44,6 +44,9 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.network.SearchUser
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -60,7 +63,8 @@ private val AVATAR_SIZE_COLLAPSED_OTHER = 36.dp
fun OtherProfileScreen(
user: SearchUser,
isDarkTheme: Boolean,
onBack: () -> Unit
onBack: () -> Unit,
avatarRepository: AvatarRepository? = null
) {
var isBlocked by remember { mutableStateOf(false) }
var showAvatarMenu by remember { mutableStateOf(false) }
@@ -198,6 +202,7 @@ fun OtherProfileScreen(
onAvatarMenuChange = { showAvatarMenu = it },
isBlocked = isBlocked,
onBlockToggle = { isBlocked = !isBlocked },
avatarRepository = avatarRepository,
onClearChat = {
viewModel.clearChatHistory()
// 🗑️ Удаляем диалог из списка после очистки истории
@@ -232,6 +237,7 @@ private fun CollapsingOtherProfileHeader(
onAvatarMenuChange: (Boolean) -> Unit,
isBlocked: Boolean,
onBlockToggle: () -> Unit,
avatarRepository: AvatarRepository? = null,
onClearChat: () -> Unit
) {
val density = LocalDensity.current
@@ -265,10 +271,18 @@ private fun CollapsingOtherProfileHeader(
modifier = Modifier
.fillMaxWidth()
.height(headerHeight)
.drawBehind {
drawRect(avatarColors.backgroundColor)
}
) {
// ═══════════════════════════════════════════════════════════
// 🎨 BLURRED AVATAR BACKGROUND
// ═══════════════════════════════════════════════════════════
BlurredAvatarBackground(
publicKey = publicKey,
avatarRepository = avatarRepository,
fallbackColor = avatarColors.backgroundColor,
blurRadius = 25f,
alpha = 0.3f
)
// ═══════════════════════════════════════════════════════════
// 🔙 BACK BUTTON
// ═══════════════════════════════════════════════════════════
@@ -346,18 +360,15 @@ private fun CollapsingOtherProfileHeader(
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.15f))
.padding(2.dp)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
.clip(CircleShape),
contentAlignment = Alignment.Center
) {
if (avatarFontSize > 1.sp) {
Text(
text = getInitials(name),
fontSize = avatarFontSize,
fontWeight = FontWeight.Bold,
color = avatarColors.textColor
)
}
AvatarImage(
publicKey = publicKey,
avatarRepository = avatarRepository,
size = avatarSize - 4.dp,
isDarkTheme = isDarkTheme
)
}
}

View File

@@ -230,9 +230,21 @@ object AvatarFileManager {
*/
fun base64ToBitmap(base64: String): Bitmap? {
return try {
val imageBytes = Base64.decode(base64, Base64.NO_WRAP)
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
// Check for data URI prefix
val actualBase64 = if (base64.contains(",")) {
android.util.Log.d("AvatarFileManager", "🔍 Removing data URI prefix, orig len=${base64.length}")
base64.substringAfter(",")
} else {
base64
}
android.util.Log.d("AvatarFileManager", "🔍 Decoding base64, len=${actualBase64.length}, prefix=${actualBase64.take(50)}")
val imageBytes = Base64.decode(actualBase64, Base64.NO_WRAP)
android.util.Log.d("AvatarFileManager", "🔍 Decoded bytes=${imageBytes.size}")
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
android.util.Log.d("AvatarFileManager", "🔍 Bitmap result=${bitmap != null}, size=${bitmap?.width}x${bitmap?.height}")
bitmap
} catch (e: Exception) {
android.util.Log.e("AvatarFileManager", "❌ base64ToBitmap error: ${e.message}")
null
}
}