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