feat: Integrate AvatarRepository into chat components for improved avatar handling and caching
This commit is contained in:
@@ -717,7 +717,8 @@ fun MainScreen(
|
|||||||
// 📨 Forward: переход в выбранный чат с полными данными
|
// 📨 Forward: переход в выбранный чат с полными данными
|
||||||
selectedUser = forwardUser
|
selectedUser = forwardUser
|
||||||
},
|
},
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme,
|
||||||
|
avatarRepository = avatarRepository
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -841,7 +842,8 @@ fun MainScreen(
|
|||||||
onBack = {
|
onBack = {
|
||||||
showOtherProfileScreen = false
|
showOtherProfileScreen = false
|
||||||
selectedOtherUser = null
|
selectedOtherUser = null
|
||||||
}
|
},
|
||||||
|
avatarRepository = avatarRepository
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ class AvatarRepository(
|
|||||||
private const val MAX_AVATAR_HISTORY = 5 // Хранить последние N аватаров
|
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)
|
// In-memory cache (как decodedAvatarsCache в desktop)
|
||||||
// publicKey -> Flow<List<AvatarInfo>>
|
// publicKey -> Flow<List<AvatarInfo>>
|
||||||
private val memoryCache = mutableMapOf<String, MutableStateFlow<List<AvatarInfo>>>()
|
private val memoryCache = mutableMapOf<String, MutableStateFlow<List<AvatarInfo>>>()
|
||||||
@@ -49,9 +54,10 @@ class AvatarRepository(
|
|||||||
val flow = MutableStateFlow<List<AvatarInfo>>(emptyList())
|
val flow = MutableStateFlow<List<AvatarInfo>>(emptyList())
|
||||||
memoryCache[publicKey] = flow
|
memoryCache[publicKey] = flow
|
||||||
|
|
||||||
// Подписываемся на изменения в БД
|
// Подписываемся на изменения в БД с использованием repository scope
|
||||||
avatarDao.getAvatars(publicKey)
|
avatarDao.getAvatars(publicKey)
|
||||||
.onEach { entities ->
|
.onEach { entities ->
|
||||||
|
Log.d(TAG, "📥 DB update for $publicKey: ${entities.size} entities")
|
||||||
val avatars = if (allDecode) {
|
val avatars = if (allDecode) {
|
||||||
// Загружаем всю историю
|
// Загружаем всю историю
|
||||||
entities.mapNotNull { entity ->
|
entities.mapNotNull { entity ->
|
||||||
@@ -64,8 +70,9 @@ class AvatarRepository(
|
|||||||
}?.let { listOf(it) } ?: emptyList()
|
}?.let { listOf(it) } ?: emptyList()
|
||||||
}
|
}
|
||||||
flow.value = avatars
|
flow.value = avatars
|
||||||
|
Log.d(TAG, "✅ Flow updated for $publicKey: ${avatars.size} avatars")
|
||||||
}
|
}
|
||||||
.launchIn(kotlinx.coroutines.CoroutineScope(Dispatchers.IO))
|
.launchIn(repositoryScope)
|
||||||
|
|
||||||
return flow.asStateFlow()
|
return flow.asStateFlow()
|
||||||
}
|
}
|
||||||
@@ -90,6 +97,7 @@ class AvatarRepository(
|
|||||||
try {
|
try {
|
||||||
// Сохраняем файл
|
// Сохраняем файл
|
||||||
val filePath = AvatarFileManager.saveAvatar(context, base64Image, fromPublicKey)
|
val filePath = AvatarFileManager.saveAvatar(context, base64Image, fromPublicKey)
|
||||||
|
Log.d(TAG, "💾 Avatar file saved for $fromPublicKey: $filePath")
|
||||||
|
|
||||||
// Сохраняем в БД
|
// Сохраняем в БД
|
||||||
val entity = AvatarCacheEntity(
|
val entity = AvatarCacheEntity(
|
||||||
@@ -98,13 +106,26 @@ class AvatarRepository(
|
|||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
avatarDao.insertAvatar(entity)
|
avatarDao.insertAvatar(entity)
|
||||||
|
Log.d(TAG, "💾 Avatar entity inserted to DB for $fromPublicKey")
|
||||||
|
|
||||||
// Очищаем старые аватары (оставляем только последние N)
|
// Очищаем старые аватары (оставляем только последние N)
|
||||||
avatarDao.deleteOldAvatars(fromPublicKey, MAX_AVATAR_HISTORY)
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to save avatar", e)
|
Log.e(TAG, "❌ Failed to save avatar for $fromPublicKey", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
|||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
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.models.*
|
||||||
import com.rosetta.messenger.ui.chats.utils.*
|
import com.rosetta.messenger.ui.chats.utils.*
|
||||||
import com.rosetta.messenger.ui.chats.components.*
|
import com.rosetta.messenger.ui.chats.components.*
|
||||||
@@ -97,7 +99,8 @@ fun ChatDetailScreen(
|
|||||||
currentUserPublicKey: String,
|
currentUserPublicKey: String,
|
||||||
currentUserPrivateKey: String,
|
currentUserPrivateKey: String,
|
||||||
totalUnreadFromOthers: Int = 0,
|
totalUnreadFromOthers: Int = 0,
|
||||||
isDarkTheme: Boolean
|
isDarkTheme: Boolean,
|
||||||
|
avatarRepository: AvatarRepository? = null
|
||||||
) {
|
) {
|
||||||
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")
|
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -780,17 +783,6 @@ fun ChatDetailScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(40.dp)
|
Modifier.size(40.dp)
|
||||||
.clip(
|
|
||||||
CircleShape
|
|
||||||
)
|
|
||||||
.background(
|
|
||||||
if (isSavedMessages
|
|
||||||
)
|
|
||||||
PrimaryBlue
|
|
||||||
else
|
|
||||||
avatarColors
|
|
||||||
.backgroundColor
|
|
||||||
)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
indication =
|
indication =
|
||||||
null,
|
null,
|
||||||
@@ -809,41 +801,32 @@ fun ChatDetailScreen(
|
|||||||
Alignment.Center
|
Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (isSavedMessages) {
|
if (isSavedMessages) {
|
||||||
Icon(
|
Box(
|
||||||
Icons.Default
|
modifier = Modifier
|
||||||
.Bookmark,
|
.fillMaxSize()
|
||||||
contentDescription =
|
.clip(CircleShape)
|
||||||
null,
|
.background(PrimaryBlue),
|
||||||
tint =
|
contentAlignment = Alignment.Center
|
||||||
Color.White,
|
) {
|
||||||
modifier =
|
Icon(
|
||||||
Modifier.size(
|
Icons.Default
|
||||||
20.dp
|
.Bookmark,
|
||||||
)
|
contentDescription =
|
||||||
)
|
null,
|
||||||
} else {
|
tint =
|
||||||
Text(
|
Color.White,
|
||||||
text =
|
modifier =
|
||||||
if (user.title
|
Modifier.size(
|
||||||
.isNotEmpty()
|
20.dp
|
||||||
)
|
|
||||||
getInitials(
|
|
||||||
user.title
|
|
||||||
)
|
)
|
||||||
else
|
)
|
||||||
user.publicKey
|
}
|
||||||
.take(
|
} else {
|
||||||
2
|
AvatarImage(
|
||||||
)
|
publicKey = user.publicKey,
|
||||||
.uppercase(),
|
avatarRepository = avatarRepository,
|
||||||
fontSize =
|
size = 40.dp,
|
||||||
14.sp,
|
isDarkTheme = isDarkTheme
|
||||||
fontWeight =
|
|
||||||
FontWeight
|
|
||||||
.Bold,
|
|
||||||
color =
|
|
||||||
avatarColors
|
|
||||||
.textColor
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1671,6 +1654,10 @@ fun ChatDetailScreen(
|
|||||||
currentUserPrivateKey,
|
currentUserPrivateKey,
|
||||||
senderPublicKey =
|
senderPublicKey =
|
||||||
if (message.isOutgoing) currentUserPublicKey else user.publicKey,
|
if (message.isOutgoing) currentUserPublicKey else user.publicKey,
|
||||||
|
currentUserPublicKey =
|
||||||
|
currentUserPublicKey,
|
||||||
|
avatarRepository =
|
||||||
|
avatarRepository,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
if (!isSelectionMode
|
if (!isSelectionMode
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ fun MessageAttachments(
|
|||||||
timestamp: java.util.Date,
|
timestamp: java.util.Date,
|
||||||
messageStatus: MessageStatus = MessageStatus.READ,
|
messageStatus: MessageStatus = MessageStatus.READ,
|
||||||
avatarRepository: AvatarRepository? = null,
|
avatarRepository: AvatarRepository? = null,
|
||||||
|
currentUserPublicKey: String = "",
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (attachments.isEmpty()) return
|
if (attachments.isEmpty()) return
|
||||||
@@ -128,6 +129,7 @@ fun MessageAttachments(
|
|||||||
privateKey = privateKey,
|
privateKey = privateKey,
|
||||||
senderPublicKey = senderPublicKey,
|
senderPublicKey = senderPublicKey,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
|
currentUserPublicKey = currentUserPublicKey,
|
||||||
isOutgoing = isOutgoing,
|
isOutgoing = isOutgoing,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
@@ -1123,6 +1125,7 @@ fun AvatarAttachment(
|
|||||||
privateKey: String,
|
privateKey: String,
|
||||||
senderPublicKey: String,
|
senderPublicKey: String,
|
||||||
avatarRepository: AvatarRepository?,
|
avatarRepository: AvatarRepository?,
|
||||||
|
currentUserPublicKey: String = "",
|
||||||
isOutgoing: Boolean,
|
isOutgoing: Boolean,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
timestamp: java.util.Date = java.util.Date(),
|
timestamp: java.util.Date = java.util.Date(),
|
||||||
@@ -1263,8 +1266,28 @@ fun AvatarAttachment(
|
|||||||
Log.d(TAG, "💾 Avatar saved to: $path")
|
Log.d(TAG, "💾 Avatar saved to: $path")
|
||||||
}
|
}
|
||||||
// Сохраняем аватар в репозиторий (для UI обновления)
|
// Сохраняем аватар в репозиторий (для 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
|
downloadStatus = DownloadStatus.DOWNLOADED
|
||||||
Log.d(TAG, "=====================================")
|
Log.d(TAG, "=====================================")
|
||||||
Log.d(TAG, "✅ AVATAR DOWNLOAD COMPLETE")
|
Log.d(TAG, "✅ AVATAR DOWNLOAD COMPLETE")
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.window.Popup
|
|||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import com.rosetta.messenger.network.AttachmentType
|
import com.rosetta.messenger.network.AttachmentType
|
||||||
import com.rosetta.messenger.network.MessageAttachment
|
import com.rosetta.messenger.network.MessageAttachment
|
||||||
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.ui.components.AppleEmojiText
|
import com.rosetta.messenger.ui.components.AppleEmojiText
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import com.rosetta.messenger.ui.chats.models.*
|
import com.rosetta.messenger.ui.chats.models.*
|
||||||
@@ -140,6 +141,8 @@ fun MessageBubble(
|
|||||||
isSavedMessages: Boolean = false,
|
isSavedMessages: Boolean = false,
|
||||||
privateKey: String = "",
|
privateKey: String = "",
|
||||||
senderPublicKey: String = "",
|
senderPublicKey: String = "",
|
||||||
|
currentUserPublicKey: String = "",
|
||||||
|
avatarRepository: AvatarRepository? = null,
|
||||||
onLongClick: () -> Unit = {},
|
onLongClick: () -> Unit = {},
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onSwipeToReply: () -> Unit = {},
|
onSwipeToReply: () -> Unit = {},
|
||||||
@@ -380,7 +383,9 @@ fun MessageBubble(
|
|||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
senderPublicKey = senderPublicKey,
|
senderPublicKey = senderPublicKey,
|
||||||
timestamp = message.timestamp,
|
timestamp = message.timestamp,
|
||||||
messageStatus = message.status
|
messageStatus = message.status,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
currentUserPublicKey = currentUserPublicKey
|
||||||
)
|
)
|
||||||
if (message.text.isNotEmpty()) {
|
if (message.text.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
@@ -70,11 +70,14 @@ fun AvatarImage(
|
|||||||
// Декодируем первый аватар
|
// Декодируем первый аватар
|
||||||
LaunchedEffect(avatars) {
|
LaunchedEffect(avatars) {
|
||||||
bitmap = if (avatars.isNotEmpty()) {
|
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) {
|
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 {
|
} else {
|
||||||
|
android.util.Log.d("AvatarImage", "⚠️ No avatars for ${publicKey.take(16)}...")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +95,11 @@ fun AvatarImage(
|
|||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
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) {
|
if (bitmap != null) {
|
||||||
// Отображаем реальный аватар
|
// Отображаем реальный аватар
|
||||||
Image(
|
Image(
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
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.network.SearchUser
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -60,7 +63,8 @@ private val AVATAR_SIZE_COLLAPSED_OTHER = 36.dp
|
|||||||
fun OtherProfileScreen(
|
fun OtherProfileScreen(
|
||||||
user: SearchUser,
|
user: SearchUser,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit,
|
||||||
|
avatarRepository: AvatarRepository? = null
|
||||||
) {
|
) {
|
||||||
var isBlocked by remember { mutableStateOf(false) }
|
var isBlocked by remember { mutableStateOf(false) }
|
||||||
var showAvatarMenu by remember { mutableStateOf(false) }
|
var showAvatarMenu by remember { mutableStateOf(false) }
|
||||||
@@ -198,6 +202,7 @@ fun OtherProfileScreen(
|
|||||||
onAvatarMenuChange = { showAvatarMenu = it },
|
onAvatarMenuChange = { showAvatarMenu = it },
|
||||||
isBlocked = isBlocked,
|
isBlocked = isBlocked,
|
||||||
onBlockToggle = { isBlocked = !isBlocked },
|
onBlockToggle = { isBlocked = !isBlocked },
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
onClearChat = {
|
onClearChat = {
|
||||||
viewModel.clearChatHistory()
|
viewModel.clearChatHistory()
|
||||||
// 🗑️ Удаляем диалог из списка после очистки истории
|
// 🗑️ Удаляем диалог из списка после очистки истории
|
||||||
@@ -232,6 +237,7 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
onAvatarMenuChange: (Boolean) -> Unit,
|
onAvatarMenuChange: (Boolean) -> Unit,
|
||||||
isBlocked: Boolean,
|
isBlocked: Boolean,
|
||||||
onBlockToggle: () -> Unit,
|
onBlockToggle: () -> Unit,
|
||||||
|
avatarRepository: AvatarRepository? = null,
|
||||||
onClearChat: () -> Unit
|
onClearChat: () -> Unit
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -265,10 +271,18 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(headerHeight)
|
.height(headerHeight)
|
||||||
.drawBehind {
|
|
||||||
drawRect(avatarColors.backgroundColor)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🎨 BLURRED AVATAR BACKGROUND
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
BlurredAvatarBackground(
|
||||||
|
publicKey = publicKey,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
fallbackColor = avatarColors.backgroundColor,
|
||||||
|
blurRadius = 25f,
|
||||||
|
alpha = 0.3f
|
||||||
|
)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 🔙 BACK BUTTON
|
// 🔙 BACK BUTTON
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -346,18 +360,15 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color.White.copy(alpha = 0.15f))
|
.background(Color.White.copy(alpha = 0.15f))
|
||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape),
|
||||||
.background(avatarColors.backgroundColor),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (avatarFontSize > 1.sp) {
|
AvatarImage(
|
||||||
Text(
|
publicKey = publicKey,
|
||||||
text = getInitials(name),
|
avatarRepository = avatarRepository,
|
||||||
fontSize = avatarFontSize,
|
size = avatarSize - 4.dp,
|
||||||
fontWeight = FontWeight.Bold,
|
isDarkTheme = isDarkTheme
|
||||||
color = avatarColors.textColor
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,9 +230,21 @@ object AvatarFileManager {
|
|||||||
*/
|
*/
|
||||||
fun base64ToBitmap(base64: String): Bitmap? {
|
fun base64ToBitmap(base64: String): Bitmap? {
|
||||||
return try {
|
return try {
|
||||||
val imageBytes = Base64.decode(base64, Base64.NO_WRAP)
|
// Check for data URI prefix
|
||||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
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) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("AvatarFileManager", "❌ base64ToBitmap error: ${e.message}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user