feat: Implement blurhash preview loading for images in ReplyBubble component

This commit is contained in:
k1ngsterr1
2026-01-28 01:48:05 +05:00
parent 5b17cff6f1
commit 3b4e4ee594
4 changed files with 116 additions and 19 deletions

View File

@@ -1408,7 +1408,10 @@ fun ChatDetailScreen(
keyboardController?.hide()
focusManager.clearFocus()
showMediaPicker = true
}
},
myPublicKey = viewModel.myPublicKey ?: "",
opponentPublicKey = user.publicKey,
myPrivateKey = currentUserPrivateKey
)
}
}

View File

@@ -625,7 +625,33 @@ fun ReplyBubble(
// Загружаем полноценную картинку вместо blur preview
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
// Blurhash preview для fallback
var blurPreviewBitmap by remember { mutableStateOf<Bitmap?>(null) }
// Сначала загружаем blurhash preview
LaunchedEffect(imageAttachment?.preview) {
if (imageAttachment != null && imageAttachment.preview.isNotEmpty()) {
withContext(Dispatchers.IO) {
try {
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
val blurhash = if (imageAttachment.preview.contains("::")) {
imageAttachment.preview.substringAfter("::")
} else if (!imageAttachment.preview.startsWith("http") && imageAttachment.preview.length < 50) {
imageAttachment.preview
} else {
""
}
if (blurhash.isNotEmpty()) {
blurPreviewBitmap = BlurHash.decode(blurhash, 36, 36)
}
} catch (e: Exception) {
// Ignore blurhash decode errors
}
}
}
}
// Потом пробуем загрузить полноценную картинку
LaunchedEffect(imageAttachment?.id) {
if (imageAttachment != null) {
withContext(Dispatchers.IO) {
@@ -742,8 +768,16 @@ fun ReplyBubble(
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else if (blurPreviewBitmap != null) {
// Blurhash preview если картинка не загружена
Image(
bitmap = blurPreviewBitmap!!.asImageBitmap(),
contentDescription = "Photo preview",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
// Placeholder с иконкой
// Placeholder с иконкой только если нет blurhash
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
@@ -920,10 +954,15 @@ private fun KebabMenuItem(
@Composable
fun ReplyImagePreview(
attachment: MessageAttachment,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
senderPublicKey: String = "",
recipientPrivateKey: String = ""
) {
val context = androidx.compose.ui.platform.LocalContext.current
var previewBitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
var fullImageBitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
// Сначала загружаем blurhash preview
LaunchedEffect(attachment.preview) {
if (attachment.preview.isNotEmpty()) {
withContext(Dispatchers.IO) {
@@ -944,12 +983,74 @@ fun ReplyImagePreview(
}
}
// Потом пробуем загрузить полноценную картинку
LaunchedEffect(attachment.id) {
if (senderPublicKey.isNotEmpty() && recipientPrivateKey.isNotEmpty()) {
withContext(Dispatchers.IO) {
try {
// Пробуем сначала из blob
if (attachment.blob.isNotEmpty()) {
val decoded = try {
val cleanBase64 = if (attachment.blob.contains(",")) {
attachment.blob.substringAfter(",")
} else {
attachment.blob
}
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
} catch (e: Exception) {
null
}
if (decoded != null) {
fullImageBitmap = decoded
return@withContext
}
}
// Если blob нет - загружаем из локального файла
val localBlob = AttachmentFileManager.readAttachment(
context,
attachment.id,
senderPublicKey,
recipientPrivateKey
)
if (localBlob != null) {
val decoded = try {
val cleanBase64 = if (localBlob.contains(",")) {
localBlob.substringAfter(",")
} else {
localBlob
}
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
} catch (e: Exception) {
null
}
fullImageBitmap = decoded
}
} catch (e: Exception) {
Log.e("ReplyImagePreview", "Failed to load full image: ${e.message}")
}
}
}
}
Box(
modifier = modifier
.clip(RoundedCornerShape(4.dp))
.background(Color.Gray.copy(alpha = 0.3f))
) {
if (previewBitmap != null) {
if (fullImageBitmap != null) {
// Полноценная картинка если загружена
Image(
bitmap = fullImageBitmap!!.asImageBitmap(),
contentDescription = "Photo preview",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else if (previewBitmap != null) {
// Blurhash preview если полная не загружена
Image(
bitmap = previewBitmap!!.asImageBitmap(),
contentDescription = "Photo preview",
@@ -957,7 +1058,7 @@ fun ReplyImagePreview(
contentScale = ContentScale.Crop
)
} else {
// Placeholder с иконкой
// Placeholder с иконкой только если нет ничего
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center

View File

@@ -216,18 +216,6 @@ fun ImageViewerScreen(
fontSize = 13.sp
)
}
// More options
IconButton(
onClick = { /* TODO: Share, save, etc */ },
modifier = Modifier.align(Alignment.CenterEnd)
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "More",
tint = Color.White
)
}
}
}

View File

@@ -75,7 +75,10 @@ fun MessageInputBar(
coordinator: KeyboardTransitionCoordinator,
displayReplyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
onReplyClick: (String) -> Unit = {},
onAttachClick: () -> Unit = {}
onAttachClick: () -> Unit = {},
myPublicKey: String = "",
opponentPublicKey: String = "",
myPrivateKey: String = ""
) {
val hasReply = replyMessages.isNotEmpty()
val keyboardController = LocalSoftwareKeyboardController.current
@@ -350,7 +353,9 @@ fun MessageInputBar(
Spacer(modifier = Modifier.width(8.dp))
ReplyImagePreview(
attachment = imageAttachment,
modifier = Modifier.size(36.dp)
modifier = Modifier.size(36.dp),
senderPublicKey = if (msg.isOutgoing) myPublicKey else opponentPublicKey,
recipientPrivateKey = myPrivateKey
)
Spacer(modifier = Modifier.width(4.dp))
}