feat: Implement avatar handling and display across chat and account screens
This commit is contained in:
@@ -583,6 +583,7 @@ fun MainScreen(
|
||||
// TODO: Show new chat screen
|
||||
},
|
||||
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
|
||||
avatarRepository = avatarRepository,
|
||||
onLogout = onLogout
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 типа:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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 - зелёный кружок с белой обводкой
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user