feat: Integrate AvatarRepository into chat components for improved avatar handling and caching

This commit is contained in:
2026-01-28 00:28:56 +05:00
parent f92bc8b0d5
commit 8702539d09
8 changed files with 142 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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

View File

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