feat: Implement avatar management system with P2P delivery

- Added AvatarRepository for handling avatar storage, retrieval, and delivery.
- Created AvatarCacheEntity and AvatarDeliveryEntity for database storage.
- Introduced PacketAvatar for P2P avatar transfer between clients.
- Enhanced RosettaDatabase to include avatar-related tables and migration.
- Developed AvatarFileManager for file operations related to avatars.
- Implemented AvatarImage composable for displaying user avatars.
- Updated ProfileScreen to support avatar selection and updating.
- Added functionality for handling incoming avatar packets in ProtocolManager.
This commit is contained in:
2026-01-23 03:04:27 +05:00
parent 6fdad7a4c1
commit b08bea2c14
12 changed files with 1670 additions and 18 deletions

View File

@@ -0,0 +1,172 @@
package com.rosetta.messenger.ui.components
import android.graphics.Bitmap
import android.util.Base64
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
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.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.chats.AvatarColors
import com.rosetta.messenger.ui.chats.getAvatarColor
import com.rosetta.messenger.ui.chats.getAvatarText
import com.rosetta.messenger.utils.AvatarFileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Composable для отображения аватара пользователя
* Совместимо с desktop версией (AvatarProvider)
*
* Приоритет отображения:
* 1. Реальный аватар из AvatarRepository (если есть)
* 2. Цветной placeholder с инициалами (fallback)
*
* @param publicKey Публичный ключ пользователя
* @param avatarRepository Репозиторий аватаров
* @param size Размер аватара
* @param isDarkTheme Темная тема
* @param onClick Обработчик клика (опционально)
* @param showOnlineIndicator Показывать индикатор онлайн
* @param isOnline Пользователь онлайн
*/
@Composable
fun AvatarImage(
publicKey: String,
avatarRepository: AvatarRepository?,
size: Dp = 40.dp,
isDarkTheme: Boolean,
onClick: (() -> Unit)? = null,
showOnlineIndicator: Boolean = false,
isOnline: Boolean = false
) {
// Получаем аватары из репозитория
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
?: remember { mutableStateOf(emptyList()) }
// Состояние для bitmap
var bitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
// Декодируем первый аватар
LaunchedEffect(avatars) {
bitmap = if (avatars.isNotEmpty()) {
withContext(Dispatchers.IO) {
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
}
} else {
null
}
}
Box(
modifier = Modifier
.size(size)
.clip(CircleShape)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
),
contentAlignment = Alignment.Center
) {
if (bitmap != null) {
// Отображаем реальный аватар
Image(
bitmap = bitmap!!.asImageBitmap(),
contentDescription = "Avatar",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
// Fallback: цветной placeholder
AvatarPlaceholder(
publicKey = publicKey,
size = size,
isDarkTheme = isDarkTheme
)
}
// Индикатор онлайн
if (showOnlineIndicator && isOnline) {
OnlineIndicator(
modifier = Modifier
.align(Alignment.BottomEnd)
.size(size / 4)
)
}
}
}
/**
* Цветной placeholder аватара с инициалами
* (используется как fallback если нет реального аватара)
*/
@Composable
fun AvatarPlaceholder(
publicKey: String,
size: Dp = 40.dp,
isDarkTheme: Boolean,
fontSize: TextUnit? = null
) {
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
val avatarText = getAvatarText(publicKey)
Box(
modifier = Modifier
.size(size)
.clip(CircleShape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
) {
Text(
text = avatarText,
color = avatarColors.textColor,
fontSize = fontSize ?: (size.value / 2.5).sp,
fontWeight = FontWeight.Medium
)
}
}
/**
* Индикатор онлайн статуса
*/
@Composable
private fun OnlineIndicator(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.clip(CircleShape)
.background(Color(0xFF4CAF50))
)
}
/**
* Composable для выбора аватара (Image Picker)
* Использует Android intent для выбора изображения
*/
@Composable
fun AvatarPicker(
onAvatarSelected: (String) -> Unit
) {
// TODO: Реализовать выбор изображения через ActivityResultContract
// 1. Использовать rememberLauncherForActivityResult с ActivityResultContracts.GetContent()
// 2. Конвертировать URI в ByteArray
// 3. Использовать AvatarFileManager.imagePrepareForNetworkTransfer()
// 4. Вызвать onAvatarSelected с Base64 PNG
}