feat: Implement blurhash preview loading for images in ReplyBubble component
This commit is contained in:
@@ -1408,7 +1408,10 @@ fun ChatDetailScreen(
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
showMediaPicker = true
|
||||
}
|
||||
},
|
||||
myPublicKey = viewModel.myPublicKey ?: "",
|
||||
opponentPublicKey = user.publicKey,
|
||||
myPrivateKey = currentUserPrivateKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user