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
|
// TODO: Show new chat screen
|
||||||
},
|
},
|
||||||
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
|
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
onLogout = onLogout
|
onLogout = onLogout
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.rosetta.messenger.crypto.CryptoManager
|
|||||||
import com.rosetta.messenger.crypto.MessageCrypto
|
import com.rosetta.messenger.crypto.MessageCrypto
|
||||||
import com.rosetta.messenger.database.*
|
import com.rosetta.messenger.database.*
|
||||||
import com.rosetta.messenger.network.*
|
import com.rosetta.messenger.network.*
|
||||||
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
@@ -53,6 +54,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
private val database = RosettaDatabase.getDatabase(context)
|
private val database = RosettaDatabase.getDatabase(context)
|
||||||
private val messageDao = database.messageDao()
|
private val messageDao = database.messageDao()
|
||||||
private val dialogDao = database.dialogDao()
|
private val dialogDao = database.dialogDao()
|
||||||
|
private val avatarDao = database.avatarDao()
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
@@ -318,7 +320,10 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
privateKey
|
privateKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// 🔒 Шифруем plainMessage с использованием приватного ключа
|
// <EFBFBD> Обрабатываем AVATAR attachments - сохраняем аватар отправителя
|
||||||
|
processAvatarAttachments(packet.attachments, packet.fromPublicKey, packet.chachaKey, privateKey)
|
||||||
|
|
||||||
|
// <20>🔒 Шифруем plainMessage с использованием приватного ключа
|
||||||
val encryptedPlainMessage = CryptoManager.encryptWithPassword(plainText, privateKey)
|
val encryptedPlainMessage = CryptoManager.encryptWithPassword(plainText, privateKey)
|
||||||
|
|
||||||
// Создаем entity для кэша и возможной вставки
|
// Создаем entity для кэша и возможной вставки
|
||||||
@@ -719,6 +724,61 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
return jsonArray.toString()
|
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 для хранения в БД
|
* Сериализация attachments в JSON с RE-ENCRYPTION для хранения в БД
|
||||||
* Для MESSAGES типа:
|
* Для MESSAGES типа:
|
||||||
|
|||||||
@@ -235,7 +235,8 @@ class PacketOnlineSubscribe : Packet() {
|
|||||||
enum class AttachmentType(val value: Int) {
|
enum class AttachmentType(val value: Int) {
|
||||||
IMAGE(0), // Изображение
|
IMAGE(0), // Изображение
|
||||||
MESSAGES(1), // Reply (цитата сообщения)
|
MESSAGES(1), // Reply (цитата сообщения)
|
||||||
FILE(2); // Файл
|
FILE(2), // Файл
|
||||||
|
AVATAR(3); // Аватар пользователя
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int) = entries.firstOrNull { it.value == value } ?: IMAGE
|
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.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
|
||||||
data class AccountInfo(
|
data class AccountInfo(
|
||||||
@@ -258,6 +262,12 @@ private fun AccountListItem(
|
|||||||
|
|
||||||
val avatarColor = getAccountColor(account.name)
|
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) }
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -288,20 +298,12 @@ private fun AccountListItem(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Avatar
|
// Avatar
|
||||||
Box(
|
AvatarImage(
|
||||||
modifier = Modifier
|
publicKey = account.publicKey,
|
||||||
.size(48.dp)
|
avatarRepository = avatarRepository,
|
||||||
.clip(CircleShape)
|
size = 48.dp,
|
||||||
.background(avatarColor.copy(alpha = 0.2f)),
|
isDarkTheme = isDarkTheme
|
||||||
contentAlignment = Alignment.Center
|
)
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = account.initials,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = avatarColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
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.AccountManager
|
||||||
import com.rosetta.messenger.data.DecryptedAccount
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
import com.rosetta.messenger.data.EncryptedAccount
|
import com.rosetta.messenger.data.EncryptedAccount
|
||||||
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
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.getAvatarColor
|
||||||
import com.rosetta.messenger.ui.chats.getAvatarText
|
import com.rosetta.messenger.ui.chats.getAvatarText
|
||||||
import com.rosetta.messenger.ui.chats.utils.getInitials
|
import com.rosetta.messenger.ui.chats.utils.getInitials
|
||||||
@@ -387,22 +390,16 @@ fun UnlockScreen(
|
|||||||
) {
|
) {
|
||||||
// Avatar
|
// Avatar
|
||||||
if (selectedAccount != null) {
|
if (selectedAccount != null) {
|
||||||
val avatarColors =
|
val database = RosettaDatabase.getDatabase(context)
|
||||||
getAvatarColor(selectedAccount!!.publicKey, isDarkTheme)
|
val avatarRepository = remember(selectedAccount!!.publicKey) {
|
||||||
Box(
|
AvatarRepository(context, database.avatarDao(), selectedAccount!!.publicKey)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
AvatarImage(
|
||||||
|
publicKey = selectedAccount!!.publicKey,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
size = 48.dp,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
@@ -572,25 +569,16 @@ fun UnlockScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Avatar
|
// Avatar
|
||||||
val avatarColors =
|
val database = RosettaDatabase.getDatabase(context)
|
||||||
getAvatarColor(account.publicKey, isDarkTheme)
|
val avatarRepository = remember(account.publicKey) {
|
||||||
Box(
|
AvatarRepository(context, database.avatarDao(), account.publicKey)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
AvatarImage(
|
||||||
|
publicKey = account.publicKey,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
size = 40.dp,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
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.ProtocolManager
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
import com.rosetta.messenger.ui.components.AppleEmojiText
|
import com.rosetta.messenger.ui.components.AppleEmojiText
|
||||||
|
import com.rosetta.messenger.ui.components.AvatarImage
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -147,6 +148,7 @@ fun ChatsListScreen(
|
|||||||
onNewChat: () -> Unit,
|
onNewChat: () -> Unit,
|
||||||
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {},
|
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {},
|
||||||
chatsViewModel: ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
|
chatsViewModel: ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
|
||||||
|
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
|
||||||
onLogout: () -> Unit
|
onLogout: () -> Unit
|
||||||
) {
|
) {
|
||||||
// Theme transition state
|
// Theme transition state
|
||||||
@@ -398,7 +400,7 @@ fun ChatsListScreen(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
// Avatar with border
|
// Avatar - используем AvatarImage
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(72.dp)
|
Modifier.size(72.dp)
|
||||||
@@ -414,29 +416,15 @@ fun ChatsListScreen(
|
|||||||
)
|
)
|
||||||
.padding(
|
.padding(
|
||||||
3.dp
|
3.dp
|
||||||
)
|
|
||||||
.clip(
|
|
||||||
CircleShape
|
|
||||||
)
|
|
||||||
.background(
|
|
||||||
avatarColors
|
|
||||||
.backgroundColor
|
|
||||||
),
|
),
|
||||||
contentAlignment =
|
contentAlignment =
|
||||||
Alignment.Center
|
Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
AvatarImage(
|
||||||
text =
|
publicKey = accountPublicKey,
|
||||||
getAvatarText(
|
avatarRepository = avatarRepository,
|
||||||
accountPublicKey
|
size = 66.dp,
|
||||||
),
|
isDarkTheme = isDarkTheme
|
||||||
fontSize = 26.sp,
|
|
||||||
fontWeight =
|
|
||||||
FontWeight
|
|
||||||
.Bold,
|
|
||||||
color =
|
|
||||||
avatarColors
|
|
||||||
.textColor
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,6 +984,8 @@ fun ChatsListScreen(
|
|||||||
isBlocked,
|
isBlocked,
|
||||||
isSavedMessages =
|
isSavedMessages =
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
|
avatarRepository =
|
||||||
|
avatarRepository,
|
||||||
onClick = {
|
onClick = {
|
||||||
val user =
|
val user =
|
||||||
chatsViewModel
|
chatsViewModel
|
||||||
@@ -1469,6 +1459,7 @@ fun SwipeableDialogItem(
|
|||||||
isTyping: Boolean = false,
|
isTyping: Boolean = false,
|
||||||
isBlocked: Boolean = false,
|
isBlocked: Boolean = false,
|
||||||
isSavedMessages: Boolean = false,
|
isSavedMessages: Boolean = false,
|
||||||
|
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onDelete: () -> Unit = {},
|
onDelete: () -> Unit = {},
|
||||||
onBlock: () -> Unit = {},
|
onBlock: () -> Unit = {},
|
||||||
@@ -1615,6 +1606,7 @@ fun SwipeableDialogItem(
|
|||||||
dialog = dialog,
|
dialog = dialog,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
isTyping = isTyping,
|
isTyping = isTyping,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1635,6 +1627,7 @@ fun DialogItemContent(
|
|||||||
dialog: DialogUiModel,
|
dialog: DialogUiModel,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
isTyping: Boolean = false,
|
isTyping: Boolean = false,
|
||||||
|
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
// 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки
|
// 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки
|
||||||
@@ -1721,31 +1714,28 @@ fun DialogItemContent(
|
|||||||
// Avatar container with online indicator
|
// Avatar container with online indicator
|
||||||
Box(modifier = Modifier.size(56.dp)) {
|
Box(modifier = Modifier.size(56.dp)) {
|
||||||
// Avatar
|
// Avatar
|
||||||
Box(
|
if (dialog.isSavedMessages) {
|
||||||
modifier =
|
Box(
|
||||||
Modifier.fillMaxSize()
|
modifier =
|
||||||
.clip(CircleShape)
|
Modifier.fillMaxSize()
|
||||||
.background(
|
.clip(CircleShape)
|
||||||
if (dialog.isSavedMessages) PrimaryBlue
|
.background(PrimaryBlue),
|
||||||
else avatarColors.backgroundColor
|
contentAlignment = Alignment.Center
|
||||||
),
|
) {
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
if (dialog.isSavedMessages) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Bookmark,
|
Icons.Default.Bookmark,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(24.dp)
|
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 - зелёный кружок с белой обводкой
|
// Online indicator - зелёный кружок с белой обводкой
|
||||||
|
|||||||
@@ -62,9 +62,15 @@ fun AvatarImage(
|
|||||||
// Состояние для bitmap
|
// Состояние для bitmap
|
||||||
var bitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
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) {
|
LaunchedEffect(avatars) {
|
||||||
bitmap = if (avatars.isNotEmpty()) {
|
bitmap = if (avatars.isNotEmpty()) {
|
||||||
|
android.util.Log.d("AvatarImage", "🔄 Decoding avatar for ${publicKey.take(16)}...")
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user