diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 2c80e40..2e2cbcd 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -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 ) } } diff --git a/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt b/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt index 7259f44..ed84a65 100644 --- a/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt @@ -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> private val memoryCache = mutableMapOf>>() @@ -49,9 +54,10 @@ class AvatarRepository( val flow = MutableStateFlow>(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) } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 2079a8e..32267fb 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -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 ) { 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 ee96783..22fcf2c 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 @@ -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") diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index 5474c12..aceba56 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -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)) diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt b/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt index 5bd0faa..7b3c879 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt @@ -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( diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt index 455c2a2..befaaa7 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt @@ -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 + ) } } 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 b202217..ff6d341 100644 --- a/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt +++ b/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt @@ -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 } }