feat: Refactor attachment components to adopt Telegram-style UI for images and files
This commit is contained in:
@@ -90,6 +90,9 @@ dependencies {
|
|||||||
implementation("io.coil-kt:coil-compose:2.5.0")
|
implementation("io.coil-kt:coil-compose:2.5.0")
|
||||||
implementation("io.coil-kt:coil-gif:2.5.0") // For animated WebP/GIF support
|
implementation("io.coil-kt:coil-gif:2.5.0") // For animated WebP/GIF support
|
||||||
|
|
||||||
|
// Blurhash for image placeholders
|
||||||
|
implementation("com.vanniktech:blurhash:0.1.0")
|
||||||
|
|
||||||
// Crypto libraries for key generation
|
// Crypto libraries for key generation
|
||||||
implementation("org.bitcoinj:bitcoinj-core:0.16.2")
|
implementation("org.bitcoinj:bitcoinj-core:0.16.2")
|
||||||
implementation("org.bouncycastle:bcprov-jdk15to18:1.77")
|
implementation("org.bouncycastle:bcprov-jdk15to18:1.77")
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ fun MessageAttachments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image attachment
|
* Image attachment - Telegram style
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageAttachment(
|
fun ImageAttachment(
|
||||||
@@ -114,26 +114,20 @@ fun ImageAttachment(
|
|||||||
privateKey: String,
|
privateKey: String,
|
||||||
isDarkTheme: Boolean
|
isDarkTheme: Boolean
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
|
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
|
||||||
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
var downloadProgress by remember { mutableStateOf(0) }
|
|
||||||
|
|
||||||
// Проверяем статус при загрузке
|
|
||||||
LaunchedEffect(attachment.id) {
|
LaunchedEffect(attachment.id) {
|
||||||
downloadStatus = if (attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview)) {
|
downloadStatus = if (attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview)) {
|
||||||
// Blob уже есть - сразу декодируем
|
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
} else if (isDownloadTag(attachment.preview)) {
|
} else if (isDownloadTag(attachment.preview)) {
|
||||||
// Нужно скачать с сервера
|
|
||||||
DownloadStatus.NOT_DOWNLOADED
|
DownloadStatus.NOT_DOWNLOADED
|
||||||
} else {
|
} else {
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если скачано - декодируем изображение
|
|
||||||
if (downloadStatus == DownloadStatus.DOWNLOADED && attachment.blob.isNotEmpty()) {
|
if (downloadStatus == DownloadStatus.DOWNLOADED && attachment.blob.isNotEmpty()) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
imageBitmap = base64ToBitmap(attachment.blob)
|
imageBitmap = base64ToBitmap(attachment.blob)
|
||||||
@@ -145,19 +139,15 @@ fun ImageAttachment(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
downloadStatus = DownloadStatus.DOWNLOADING
|
downloadStatus = DownloadStatus.DOWNLOADING
|
||||||
|
|
||||||
val tag = getDownloadTag(attachment.preview)
|
val tag = getDownloadTag(attachment.preview)
|
||||||
if (tag.isEmpty()) {
|
if (tag.isEmpty()) {
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скачиваем с транспортного сервера
|
|
||||||
val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
|
val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
|
||||||
|
|
||||||
downloadStatus = DownloadStatus.DECRYPTING
|
downloadStatus = DownloadStatus.DECRYPTING
|
||||||
|
|
||||||
// Расшифровываем с ChaCha ключом
|
|
||||||
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
||||||
encryptedContent,
|
encryptedContent,
|
||||||
chachaKey,
|
chachaKey,
|
||||||
@@ -179,71 +169,64 @@ fun ImageAttachment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
// Telegram-style: Изображение без Card wrapper
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.widthIn(min = 200.dp, max = 280.dp)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.heightIn(min = 150.dp, max = 350.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
|
if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
|
||||||
download()
|
download()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = CardDefaults.cardColors(
|
contentAlignment = Alignment.Center
|
||||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
when (downloadStatus) {
|
||||||
modifier = Modifier
|
DownloadStatus.DOWNLOADED -> {
|
||||||
.fillMaxWidth()
|
imageBitmap?.let { bitmap ->
|
||||||
.heightIn(min = 100.dp, max = 300.dp),
|
Image(
|
||||||
contentAlignment = Alignment.Center
|
bitmap = bitmap.asImageBitmap(),
|
||||||
) {
|
contentDescription = "Image",
|
||||||
when (downloadStatus) {
|
modifier = Modifier.fillMaxSize(),
|
||||||
DownloadStatus.DOWNLOADED -> {
|
contentScale = ContentScale.Crop
|
||||||
imageBitmap?.let { bitmap ->
|
)
|
||||||
Image(
|
|
||||||
bitmap = bitmap.asImageBitmap(),
|
|
||||||
contentDescription = "Image",
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
contentScale = ContentScale.Fit
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
DownloadStatus.NOT_DOWNLOADED -> {
|
}
|
||||||
// Показываем preview (blurhash) если есть
|
DownloadStatus.NOT_DOWNLOADED -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Image,
|
Icons.Default.Download,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (isDarkTheme) Color.White.copy(alpha = 0.7f) else Color.Gray,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier.size(48.dp)
|
modifier = Modifier.size(40.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Button(
|
Text(
|
||||||
onClick = download,
|
"Tap to download",
|
||||||
colors = ButtonDefaults.buttonColors(
|
fontSize = 13.sp,
|
||||||
containerColor = PrimaryBlue
|
color = if (isDarkTheme) Color.White.copy(0.7f) else Color.Gray
|
||||||
)
|
)
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Download,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text("Download")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
|
}
|
||||||
|
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
color = PrimaryBlue,
|
color = PrimaryBlue,
|
||||||
@@ -253,32 +236,44 @@ fun ImageAttachment(
|
|||||||
Text(
|
Text(
|
||||||
text = if (downloadStatus == DownloadStatus.DECRYPTING) "Decrypting..." else "Downloading...",
|
text = if (downloadStatus == DownloadStatus.DECRYPTING) "Decrypting..." else "Downloading...",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.7f) else Color.Gray
|
color = if (isDarkTheme) Color.White.copy(0.7f) else Color.Gray
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DownloadStatus.ERROR -> {
|
}
|
||||||
|
DownloadStatus.ERROR -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Error,
|
Icons.Default.Error,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.Red,
|
tint = Color(0xFFE53935),
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text("Download failed", color = Color.Red, fontSize = 12.sp)
|
Text("Failed", fontSize = 12.sp, color = Color(0xFFE53935))
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
TextButton(onClick = download) {
|
TextButton(onClick = download) {
|
||||||
Text("Retry")
|
Text("Retry", fontSize = 12.sp, color = PrimaryBlue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
}
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
else -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp), color = PrimaryBlue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,7 +281,7 @@ fun ImageAttachment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File attachment
|
* File attachment - Telegram style
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun FileAttachment(
|
fun FileAttachment(
|
||||||
@@ -298,12 +293,10 @@ fun FileAttachment(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
|
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
|
||||||
|
|
||||||
// Парсим метаданные: "filesize::filename"
|
|
||||||
val preview = attachment.preview
|
val preview = attachment.preview
|
||||||
val parts = preview.split("::")
|
val parts = preview.split("::")
|
||||||
val (fileSize, fileName) = when {
|
val (fileSize, fileName) = when {
|
||||||
parts.size >= 3 -> {
|
parts.size >= 3 -> {
|
||||||
// Формат: "UUID::filesize::filename"
|
|
||||||
val size = parts[1].toLongOrNull() ?: 0L
|
val size = parts[1].toLongOrNull() ?: 0L
|
||||||
val name = parts.drop(2).joinToString("::")
|
val name = parts.drop(2).joinToString("::")
|
||||||
Pair(size, name)
|
Pair(size, name)
|
||||||
@@ -315,7 +308,6 @@ fun FileAttachment(
|
|||||||
else -> Pair(0L, "File")
|
else -> Pair(0L, "File")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем статус
|
|
||||||
LaunchedEffect(attachment.id) {
|
LaunchedEffect(attachment.id) {
|
||||||
downloadStatus = if (isDownloadTag(preview)) {
|
downloadStatus = if (isDownloadTag(preview)) {
|
||||||
DownloadStatus.NOT_DOWNLOADED
|
DownloadStatus.NOT_DOWNLOADED
|
||||||
@@ -328,7 +320,6 @@ fun FileAttachment(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
downloadStatus = DownloadStatus.DOWNLOADING
|
downloadStatus = DownloadStatus.DOWNLOADING
|
||||||
|
|
||||||
val tag = getDownloadTag(preview)
|
val tag = getDownloadTag(preview)
|
||||||
if (tag.isEmpty()) {
|
if (tag.isEmpty()) {
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
@@ -336,7 +327,6 @@ fun FileAttachment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
|
val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
|
||||||
|
|
||||||
downloadStatus = DownloadStatus.DECRYPTING
|
downloadStatus = DownloadStatus.DECRYPTING
|
||||||
|
|
||||||
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
||||||
@@ -346,7 +336,6 @@ fun FileAttachment(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (decrypted != null) {
|
if (decrypted != null) {
|
||||||
// TODO: Сохранить файл в Downloads
|
|
||||||
downloadStatus = DownloadStatus.DOWNLOADED
|
downloadStatus = DownloadStatus.DOWNLOADED
|
||||||
} else {
|
} else {
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
@@ -358,102 +347,97 @@ fun FileAttachment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
// Telegram-style: компактный row с иконкой файла
|
||||||
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(if (isDarkTheme) Color(0xFF1F1F1F) else Color.White.copy(0.3f))
|
||||||
.clickable {
|
.clickable {
|
||||||
if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
|
if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
|
||||||
download()
|
download()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
colors = CardDefaults.cardColors(
|
.padding(10.dp),
|
||||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
// File icon
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.size(44.dp)
|
||||||
.padding(12.dp),
|
.clip(RoundedCornerShape(6.dp))
|
||||||
verticalAlignment = Alignment.CenterVertically
|
.background(PrimaryBlue.copy(alpha = 0.15f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// File icon
|
Icon(
|
||||||
Box(
|
Icons.Default.InsertDriveFile,
|
||||||
modifier = Modifier
|
contentDescription = null,
|
||||||
.size(48.dp)
|
tint = PrimaryBlue,
|
||||||
.clip(RoundedCornerShape(8.dp))
|
modifier = Modifier.size(22.dp)
|
||||||
.background(PrimaryBlue.copy(alpha = 0.2f)),
|
)
|
||||||
contentAlignment = Alignment.Center
|
}
|
||||||
) {
|
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
|
||||||
|
// File info
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = fileName,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (isDarkTheme) Color.White else Color.Black,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatFileSize(fileSize),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// Download/status icon
|
||||||
|
when (downloadStatus) {
|
||||||
|
DownloadStatus.NOT_DOWNLOADED -> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.InsertDriveFile,
|
Icons.Default.Download,
|
||||||
contentDescription = null,
|
contentDescription = "Download",
|
||||||
tint = PrimaryBlue,
|
tint = PrimaryBlue,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
// File info
|
color = PrimaryBlue,
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
strokeWidth = 2.dp
|
||||||
Text(
|
|
||||||
text = fileName,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = if (isDarkTheme) Color.White else Color.Black,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = formatFileSize(fileSize),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Gray
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
DownloadStatus.DOWNLOADED -> {
|
||||||
// Download button/status
|
Icon(
|
||||||
when (downloadStatus) {
|
Icons.Default.CheckCircle,
|
||||||
DownloadStatus.NOT_DOWNLOADED -> {
|
contentDescription = "Downloaded",
|
||||||
IconButton(onClick = download) {
|
tint = Color(0xFF4CAF50),
|
||||||
Icon(
|
modifier = Modifier.size(24.dp)
|
||||||
Icons.Default.Download,
|
)
|
||||||
contentDescription = "Download",
|
|
||||||
tint = PrimaryBlue
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
color = PrimaryBlue,
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DownloadStatus.DOWNLOADED -> {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.CheckCircle,
|
|
||||||
contentDescription = "Downloaded",
|
|
||||||
tint = Color(0xFF4CAF50)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DownloadStatus.ERROR -> {
|
|
||||||
IconButton(onClick = download) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Refresh,
|
|
||||||
contentDescription = "Retry",
|
|
||||||
tint = Color.Red
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
DownloadStatus.ERROR -> {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Refresh,
|
||||||
|
contentDescription = "Retry",
|
||||||
|
tint = Color(0xFFE53935),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Avatar attachment - аватар отправителя
|
* Avatar attachment - Telegram style (более компактный)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AvatarAttachment(
|
fun AvatarAttachment(
|
||||||
@@ -464,7 +448,6 @@ fun AvatarAttachment(
|
|||||||
avatarRepository: AvatarRepository?,
|
avatarRepository: AvatarRepository?,
|
||||||
isDarkTheme: Boolean
|
isDarkTheme: Boolean
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
|
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
|
||||||
@@ -490,7 +473,6 @@ fun AvatarAttachment(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
downloadStatus = DownloadStatus.DOWNLOADING
|
downloadStatus = DownloadStatus.DOWNLOADING
|
||||||
|
|
||||||
val tag = getDownloadTag(attachment.preview)
|
val tag = getDownloadTag(attachment.preview)
|
||||||
if (tag.isEmpty()) {
|
if (tag.isEmpty()) {
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
@@ -498,7 +480,6 @@ fun AvatarAttachment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
|
val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
|
||||||
|
|
||||||
downloadStatus = DownloadStatus.DECRYPTING
|
downloadStatus = DownloadStatus.DECRYPTING
|
||||||
|
|
||||||
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
||||||
@@ -511,7 +492,6 @@ fun AvatarAttachment(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
avatarBitmap = base64ToBitmap(decrypted)
|
avatarBitmap = base64ToBitmap(decrypted)
|
||||||
}
|
}
|
||||||
// Сохраняем аватар в кэш
|
|
||||||
avatarRepository?.saveAvatar(senderPublicKey, decrypted)
|
avatarRepository?.saveAvatar(senderPublicKey, decrypted)
|
||||||
downloadStatus = DownloadStatus.DOWNLOADED
|
downloadStatus = DownloadStatus.DOWNLOADED
|
||||||
} else {
|
} else {
|
||||||
@@ -524,99 +504,107 @@ fun AvatarAttachment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
// Telegram-style: компактный row с круглым превью
|
||||||
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
.clip(RoundedCornerShape(8.dp))
|
||||||
colors = CardDefaults.cardColors(
|
.background(if (isDarkTheme) Color(0xFF1F1F1F) else Color.White.copy(0.3f))
|
||||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8)
|
.clickable(enabled = downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
|
||||||
)
|
download()
|
||||||
|
}
|
||||||
|
.padding(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
// Avatar preview (круглое)
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.size(50.dp)
|
||||||
.padding(12.dp),
|
.clip(CircleShape)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// Avatar preview
|
when (downloadStatus) {
|
||||||
Box(
|
DownloadStatus.DOWNLOADED -> {
|
||||||
modifier = Modifier
|
avatarBitmap?.let { bitmap ->
|
||||||
.size(60.dp)
|
Image(
|
||||||
.clip(CircleShape)
|
bitmap = bitmap.asImageBitmap(),
|
||||||
.background(if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFD0D0D0)),
|
contentDescription = "Avatar",
|
||||||
contentAlignment = Alignment.Center
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
contentScale = ContentScale.Crop
|
||||||
when (downloadStatus) {
|
|
||||||
DownloadStatus.DOWNLOADED -> {
|
|
||||||
avatarBitmap?.let { bitmap ->
|
|
||||||
Image(
|
|
||||||
bitmap = bitmap.asImageBitmap(),
|
|
||||||
contentDescription = "Avatar",
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
color = PrimaryBlue,
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Person,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray,
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
color = PrimaryBlue,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isDarkTheme) Color.White.copy(0.4f) else Color.Gray,
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
|
||||||
text = "Avatar",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Lock,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = PrimaryBlue,
|
|
||||||
modifier = Modifier.size(14.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
text = "An avatar image shared in the message.",
|
text = "Avatar",
|
||||||
fontSize = 12.sp,
|
fontSize = 14.sp,
|
||||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Gray
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Lock,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isDarkTheme) Color.White.copy(0.5f) else Color.Gray,
|
||||||
|
modifier = Modifier.size(12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
text = "Profile photo shared",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = if (isDarkTheme) Color.White.copy(0.5f) else Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
|
// Download/status icon
|
||||||
Button(
|
when (downloadStatus) {
|
||||||
onClick = download,
|
DownloadStatus.NOT_DOWNLOADED -> {
|
||||||
colors = ButtonDefaults.buttonColors(
|
Icon(
|
||||||
containerColor = PrimaryBlue
|
Icons.Default.Download,
|
||||||
),
|
contentDescription = "Download",
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp)
|
tint = PrimaryBlue,
|
||||||
) {
|
modifier = Modifier.size(24.dp)
|
||||||
Icon(
|
)
|
||||||
Icons.Default.Download,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text("Download", fontSize = 12.sp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
DownloadStatus.DOWNLOADED -> {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CheckCircle,
|
||||||
|
contentDescription = "Downloaded",
|
||||||
|
tint = Color(0xFF4CAF50),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DownloadStatus.ERROR -> {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Refresh,
|
||||||
|
contentDescription = "Retry",
|
||||||
|
tint = Color(0xFFE53935),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user