From 10c78e62316b395e1522575850a759f8c39a209f Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sat, 24 Jan 2026 01:14:25 +0500 Subject: [PATCH] feat: Implement avatar handling and display across chat and account screens --- .../com/rosetta/messenger/MainActivity.kt | 1 + .../messenger/data/MessageRepository.kt | 62 ++++++++++++++++- .../com/rosetta/messenger/network/Packets.kt | 3 +- .../messenger/ui/auth/SelectAccountScreen.kt | 30 +++++---- .../rosetta/messenger/ui/auth/UnlockScreen.kt | 54 ++++++--------- .../messenger/ui/chats/ChatsListScreen.kt | 66 ++++++++----------- .../messenger/ui/components/AvatarImage.kt | 6 ++ 7 files changed, 135 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index e9d1631..26753fb 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -583,6 +583,7 @@ fun MainScreen( // TODO: Show new chat screen }, onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser }, + avatarRepository = avatarRepository, onLogout = onLogout ) } diff --git a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt index 3653dd6..67bb466 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -6,6 +6,7 @@ import com.rosetta.messenger.crypto.CryptoManager import com.rosetta.messenger.crypto.MessageCrypto import com.rosetta.messenger.database.* import com.rosetta.messenger.network.* +import com.rosetta.messenger.utils.AvatarFileManager import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.json.JSONArray @@ -53,6 +54,7 @@ class MessageRepository private constructor(private val context: Context) { private val database = RosettaDatabase.getDatabase(context) private val messageDao = database.messageDao() private val dialogDao = database.dialogDao() + private val avatarDao = database.avatarDao() private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -318,7 +320,10 @@ class MessageRepository private constructor(private val context: Context) { privateKey ) - // 🔒 Шифруем plainMessage с использованием приватного ключа + // � Обрабатываем AVATAR attachments - сохраняем аватар отправителя + processAvatarAttachments(packet.attachments, packet.fromPublicKey, packet.chachaKey, privateKey) + + // �🔒 Шифруем plainMessage с использованием приватного ключа val encryptedPlainMessage = CryptoManager.encryptWithPassword(plainText, privateKey) // Создаем entity для кэша и возможной вставки @@ -719,6 +724,61 @@ class MessageRepository private constructor(private val context: Context) { return jsonArray.toString() } + /** + * 📸 Обработка AVATAR attachments - сохранение аватара отправителя в кэш + * Как в desktop: при получении attachment с типом AVATAR - сохраняем в avatar_cache + */ + private suspend fun processAvatarAttachments( + attachments: List, + fromPublicKey: String, + encryptedKey: String, + privateKey: String + ) { + Log.d("MessageRepository", "📸 processAvatarAttachments: ${attachments.size} attachments from ${fromPublicKey.take(16)}") + + for (attachment in attachments) { + Log.d("MessageRepository", "📸 Attachment type=${attachment.type} (AVATAR=${AttachmentType.AVATAR}), blob size=${attachment.blob.length}") + + if (attachment.type == AttachmentType.AVATAR && attachment.blob.isNotEmpty()) { + try { + Log.d("MessageRepository", "📸 Found AVATAR attachment! Decrypting...") + + // 1. Расшифровываем blob с ChaCha ключом сообщения + val decryptedBlob = MessageCrypto.decryptAttachmentBlob( + attachment.blob, + encryptedKey, + privateKey + ) + + Log.d("MessageRepository", "📸 Decrypted blob: ${decryptedBlob?.take(50) ?: "NULL"}") + + if (decryptedBlob != null) { + // 2. Сохраняем аватар в кэш + val filePath = AvatarFileManager.saveAvatar(context, decryptedBlob, fromPublicKey) + Log.d("MessageRepository", "📸 Avatar saved to: $filePath") + + val entity = AvatarCacheEntity( + publicKey = fromPublicKey, + avatar = filePath, + timestamp = System.currentTimeMillis() + ) + avatarDao.insertAvatar(entity) + Log.d("MessageRepository", "📸 Avatar inserted to DB for ${fromPublicKey.take(16)}") + + // 3. Очищаем старые аватары (оставляем последние 5) + avatarDao.deleteOldAvatars(fromPublicKey, 5) + + Log.d("MessageRepository", "📸 ✅ Successfully saved avatar for $fromPublicKey") + } else { + Log.w("MessageRepository", "📸 ⚠️ Decryption returned null!") + } + } catch (e: Exception) { + Log.e("MessageRepository", "📸 ❌ Failed to process avatar attachment", e) + } + } + } + } + /** * Сериализация attachments в JSON с RE-ENCRYPTION для хранения в БД * Для MESSAGES типа: diff --git a/app/src/main/java/com/rosetta/messenger/network/Packets.kt b/app/src/main/java/com/rosetta/messenger/network/Packets.kt index 50e6bfd..5677a18 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Packets.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Packets.kt @@ -235,7 +235,8 @@ class PacketOnlineSubscribe : Packet() { enum class AttachmentType(val value: Int) { IMAGE(0), // Изображение MESSAGES(1), // Reply (цитата сообщения) - FILE(2); // Файл + FILE(2), // Файл + AVATAR(3); // Аватар пользователя companion object { fun fromInt(value: Int) = entries.firstOrNull { it.value == value } ?: IMAGE diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SelectAccountScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SelectAccountScreen.kt index dbbe4cb..e1cd8b8 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SelectAccountScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SelectAccountScreen.kt @@ -16,11 +16,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.rosetta.messenger.database.RosettaDatabase +import com.rosetta.messenger.repository.AvatarRepository +import com.rosetta.messenger.ui.components.AvatarImage import com.rosetta.messenger.ui.onboarding.PrimaryBlue data class AccountInfo( @@ -258,6 +262,12 @@ private fun AccountListItem( val avatarColor = getAccountColor(account.name) + val context = LocalContext.current + val avatarRepository = remember(account.publicKey) { + val database = RosettaDatabase.getDatabase(context) + AvatarRepository(context, database.avatarDao(), account.publicKey) + } + var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -288,20 +298,12 @@ private fun AccountListItem( verticalAlignment = Alignment.CenterVertically ) { // Avatar - Box( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .background(avatarColor.copy(alpha = 0.2f)), - contentAlignment = Alignment.Center - ) { - Text( - text = account.initials, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, - color = avatarColor - ) - } + AvatarImage( + publicKey = account.publicKey, + avatarRepository = avatarRepository, + size = 48.dp, + isDarkTheme = isDarkTheme + ) Spacer(modifier = Modifier.width(16.dp)) diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index e660fe3..e337f31 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -41,8 +41,11 @@ import com.rosetta.messenger.crypto.CryptoManager import com.rosetta.messenger.data.AccountManager import com.rosetta.messenger.data.DecryptedAccount import com.rosetta.messenger.data.EncryptedAccount +import com.rosetta.messenger.database.RosettaDatabase import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolState +import com.rosetta.messenger.repository.AvatarRepository +import com.rosetta.messenger.ui.components.AvatarImage import com.rosetta.messenger.ui.chats.getAvatarColor import com.rosetta.messenger.ui.chats.getAvatarText import com.rosetta.messenger.ui.chats.utils.getInitials @@ -387,22 +390,16 @@ fun UnlockScreen( ) { // Avatar if (selectedAccount != null) { - val avatarColors = - getAvatarColor(selectedAccount!!.publicKey, isDarkTheme) - Box( - modifier = - Modifier.size(48.dp) - .clip(CircleShape) - .background(avatarColors.backgroundColor), - contentAlignment = Alignment.Center - ) { - Text( - text = getInitials(selectedAccount!!.name), - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - color = avatarColors.textColor - ) + val database = RosettaDatabase.getDatabase(context) + val avatarRepository = remember(selectedAccount!!.publicKey) { + AvatarRepository(context, database.avatarDao(), selectedAccount!!.publicKey) } + AvatarImage( + publicKey = selectedAccount!!.publicKey, + avatarRepository = avatarRepository, + size = 48.dp, + isDarkTheme = isDarkTheme + ) } Spacer(modifier = Modifier.width(12.dp)) @@ -572,25 +569,16 @@ fun UnlockScreen( verticalAlignment = Alignment.CenterVertically ) { // Avatar - val avatarColors = - getAvatarColor(account.publicKey, isDarkTheme) - Box( - modifier = - Modifier.size(40.dp) - .clip(CircleShape) - .background( - avatarColors - .backgroundColor - ), - contentAlignment = Alignment.Center - ) { - Text( - text = getInitials(account.name), - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = avatarColors.textColor - ) + val database = RosettaDatabase.getDatabase(context) + val avatarRepository = remember(account.publicKey) { + AvatarRepository(context, database.avatarDao(), account.publicKey) } + AvatarImage( + publicKey = account.publicKey, + avatarRepository = avatarRepository, + size = 40.dp, + isDarkTheme = isDarkTheme + ) Spacer(modifier = Modifier.width(12.dp)) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index d94a298..18dfb7d 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -37,6 +37,7 @@ import com.rosetta.messenger.data.RecentSearchesManager import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolState import com.rosetta.messenger.ui.components.AppleEmojiText +import com.rosetta.messenger.ui.components.AvatarImage import com.rosetta.messenger.ui.onboarding.PrimaryBlue import java.text.SimpleDateFormat import java.util.* @@ -147,6 +148,7 @@ fun ChatsListScreen( onNewChat: () -> Unit, onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {}, chatsViewModel: ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(), + avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, onLogout: () -> Unit ) { // Theme transition state @@ -398,7 +400,7 @@ fun ChatsListScreen( ) ) { Column { - // Avatar with border + // Avatar - используем AvatarImage Box( modifier = Modifier.size(72.dp) @@ -414,29 +416,15 @@ fun ChatsListScreen( ) .padding( 3.dp - ) - .clip( - CircleShape - ) - .background( - avatarColors - .backgroundColor ), contentAlignment = Alignment.Center ) { - Text( - text = - getAvatarText( - accountPublicKey - ), - fontSize = 26.sp, - fontWeight = - FontWeight - .Bold, - color = - avatarColors - .textColor + AvatarImage( + publicKey = accountPublicKey, + avatarRepository = avatarRepository, + size = 66.dp, + isDarkTheme = isDarkTheme ) } @@ -996,6 +984,8 @@ fun ChatsListScreen( isBlocked, isSavedMessages = isSavedMessages, + avatarRepository = + avatarRepository, onClick = { val user = chatsViewModel @@ -1469,6 +1459,7 @@ fun SwipeableDialogItem( isTyping: Boolean = false, isBlocked: Boolean = false, isSavedMessages: Boolean = false, + avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, onClick: () -> Unit, onDelete: () -> Unit = {}, onBlock: () -> Unit = {}, @@ -1615,6 +1606,7 @@ fun SwipeableDialogItem( dialog = dialog, isDarkTheme = isDarkTheme, isTyping = isTyping, + avatarRepository = avatarRepository, onClick = onClick ) @@ -1635,6 +1627,7 @@ fun DialogItemContent( dialog: DialogUiModel, isDarkTheme: Boolean, isTyping: Boolean = false, + avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, onClick: () -> Unit ) { // 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки @@ -1721,31 +1714,28 @@ fun DialogItemContent( // Avatar container with online indicator Box(modifier = Modifier.size(56.dp)) { // Avatar - Box( - modifier = - Modifier.fillMaxSize() - .clip(CircleShape) - .background( - if (dialog.isSavedMessages) PrimaryBlue - else avatarColors.backgroundColor - ), - contentAlignment = Alignment.Center - ) { - if (dialog.isSavedMessages) { + if (dialog.isSavedMessages) { + Box( + modifier = + Modifier.fillMaxSize() + .clip(CircleShape) + .background(PrimaryBlue), + contentAlignment = Alignment.Center + ) { Icon( Icons.Default.Bookmark, contentDescription = null, tint = Color.White, modifier = Modifier.size(24.dp) ) - } else { - Text( - text = initials, - color = avatarColors.textColor, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp - ) } + } else { + com.rosetta.messenger.ui.components.AvatarImage( + publicKey = dialog.opponentKey, + avatarRepository = avatarRepository, + size = 56.dp, + isDarkTheme = isDarkTheme + ) } // Online indicator - зелёный кружок с белой обводкой 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 06de7f4..5bd0faa 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 @@ -62,9 +62,15 @@ fun AvatarImage( // Состояние для bitmap var bitmap by remember(avatars) { mutableStateOf(null) } + // Логируем для отладки + LaunchedEffect(publicKey, avatars) { + android.util.Log.d("AvatarImage", "📸 publicKey=${publicKey.take(16)}... avatars=${avatars.size} repository=${avatarRepository != null}") + } + // Декодируем первый аватар LaunchedEffect(avatars) { bitmap = if (avatars.isNotEmpty()) { + android.util.Log.d("AvatarImage", "🔄 Decoding avatar for ${publicKey.take(16)}...") withContext(Dispatchers.IO) { AvatarFileManager.base64ToBitmap(avatars.first().base64Data) }