feat: Implement blurhash preview loading for images in ReplyBubble component
This commit is contained in:
@@ -1408,7 +1408,10 @@ fun ChatDetailScreen(
|
|||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
showMediaPicker = true
|
showMediaPicker = true
|
||||||
}
|
},
|
||||||
|
myPublicKey = viewModel.myPublicKey ?: "",
|
||||||
|
opponentPublicKey = user.publicKey,
|
||||||
|
myPrivateKey = currentUserPrivateKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -625,7 +625,33 @@ fun ReplyBubble(
|
|||||||
|
|
||||||
// Загружаем полноценную картинку вместо blur preview
|
// Загружаем полноценную картинку вместо blur preview
|
||||||
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
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) {
|
LaunchedEffect(imageAttachment?.id) {
|
||||||
if (imageAttachment != null) {
|
if (imageAttachment != null) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -742,8 +768,16 @@ fun ReplyBubble(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
} else if (blurPreviewBitmap != null) {
|
||||||
|
// Blurhash preview если картинка не загружена
|
||||||
|
Image(
|
||||||
|
bitmap = blurPreviewBitmap!!.asImageBitmap(),
|
||||||
|
contentDescription = "Photo preview",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// Placeholder с иконкой
|
// Placeholder с иконкой только если нет blurhash
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -920,10 +954,15 @@ private fun KebabMenuItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ReplyImagePreview(
|
fun ReplyImagePreview(
|
||||||
attachment: MessageAttachment,
|
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 previewBitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
|
||||||
|
var fullImageBitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
|
||||||
|
|
||||||
|
// Сначала загружаем blurhash preview
|
||||||
LaunchedEffect(attachment.preview) {
|
LaunchedEffect(attachment.preview) {
|
||||||
if (attachment.preview.isNotEmpty()) {
|
if (attachment.preview.isNotEmpty()) {
|
||||||
withContext(Dispatchers.IO) {
|
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(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.background(Color.Gray.copy(alpha = 0.3f))
|
.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(
|
Image(
|
||||||
bitmap = previewBitmap!!.asImageBitmap(),
|
bitmap = previewBitmap!!.asImageBitmap(),
|
||||||
contentDescription = "Photo preview",
|
contentDescription = "Photo preview",
|
||||||
@@ -957,7 +1058,7 @@ fun ReplyImagePreview(
|
|||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Placeholder с иконкой
|
// Placeholder с иконкой только если нет ничего
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
|||||||
@@ -216,18 +216,6 @@ fun ImageViewerScreen(
|
|||||||
fontSize = 13.sp
|
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,
|
coordinator: KeyboardTransitionCoordinator,
|
||||||
displayReplyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
|
displayReplyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
|
||||||
onReplyClick: (String) -> Unit = {},
|
onReplyClick: (String) -> Unit = {},
|
||||||
onAttachClick: () -> Unit = {}
|
onAttachClick: () -> Unit = {},
|
||||||
|
myPublicKey: String = "",
|
||||||
|
opponentPublicKey: String = "",
|
||||||
|
myPrivateKey: String = ""
|
||||||
) {
|
) {
|
||||||
val hasReply = replyMessages.isNotEmpty()
|
val hasReply = replyMessages.isNotEmpty()
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
@@ -350,7 +353,9 @@ fun MessageInputBar(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
ReplyImagePreview(
|
ReplyImagePreview(
|
||||||
attachment = imageAttachment,
|
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))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user