feat: Implement avatar handling and display across chat and account screens

This commit is contained in:
k1ngsterr1
2026-01-24 01:14:25 +05:00
parent 1367864008
commit 10c78e6231
7 changed files with 135 additions and 87 deletions

View File

@@ -583,6 +583,7 @@ fun MainScreen(
// TODO: Show new chat screen
},
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
avatarRepository = avatarRepository,
onLogout = onLogout
)
}

View File

@@ -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 с использованием приватного ключа
// <EFBFBD> Обрабатываем AVATAR attachments - сохраняем аватар отправителя
processAvatarAttachments(packet.attachments, packet.fromPublicKey, packet.chachaKey, privateKey)
// <20>🔒 Шифруем 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<MessageAttachment>,
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 типа:

View File

@@ -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

View File

@@ -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))

View File

@@ -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))

View File

@@ -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 - зелёный кружок с белой обводкой

View File

@@ -62,9 +62,15 @@ fun AvatarImage(
// Состояние для bitmap
var bitmap by remember(avatars) { mutableStateOf<Bitmap?>(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)
}