feat: Enhance reply functionality with public and private key support for attachment decryption

This commit is contained in:
k1ngsterr1
2026-01-28 01:21:57 +05:00
parent a8f5ae63fd
commit e6f88952c7
3 changed files with 65 additions and 18 deletions

View File

@@ -851,7 +851,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
messageId = "",
senderName = opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } },
text = replyText,
isFromMe = false // Мы не знаем кто автор из fallback формата
isFromMe = false, // Мы не знаем кто автор из fallback формата
senderPublicKey = opponentKey ?: "",
recipientPrivateKey = myPrivateKey ?: ""
),
mainText
)
@@ -1005,7 +1007,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
senderName = if (isReplyFromMe) "You" else opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } },
text = replyText,
isFromMe = isReplyFromMe,
attachments = originalAttachments
attachments = originalAttachments,
senderPublicKey = if (isReplyFromMe) myPublicKey ?: "" else opponentKey ?: "",
recipientPrivateKey = myPrivateKey ?: ""
)
return result
} else {
@@ -1171,7 +1175,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
text = firstReply.text,
isFromMe = firstReply.isOutgoing,
isForwarded = isForward,
attachments = replyAttachments
attachments = replyAttachments,
senderPublicKey = if (firstReply.isOutgoing) myPublicKey ?: "" else opponentKey ?: "",
recipientPrivateKey = myPrivateKey ?: ""
)
} else null

View File

@@ -39,13 +39,19 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import android.graphics.BitmapFactory
import android.util.Base64
import android.util.Log
import com.rosetta.messenger.crypto.MessageCrypto
import com.rosetta.messenger.network.AttachmentType
import com.rosetta.messenger.network.MessageAttachment
import com.rosetta.messenger.network.TransportManager
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.components.AppleEmojiText
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.chats.models.*
import com.rosetta.messenger.ui.chats.utils.*
import com.rosetta.messenger.utils.AttachmentFileManager
import com.vanniktech.blurhash.BlurHash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -598,6 +604,7 @@ fun ReplyBubble(
isDarkTheme: Boolean,
onClick: () -> Unit = {}
) {
val context = androidx.compose.ui.platform.LocalContext.current
val backgroundColor = if (isOutgoing) {
Color.Black.copy(alpha = 0.15f)
} else {
@@ -616,24 +623,56 @@ fun ReplyBubble(
val imageAttachment = replyData.attachments.firstOrNull { it.type == AttachmentType.IMAGE }
val hasImage = imageAttachment != null
// Декодируем blurhash для превью
var previewBitmap by remember { mutableStateOf<Bitmap?>(null) }
// Загружаем полноценную картинку вместо blur preview
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
LaunchedEffect(imageAttachment?.preview) {
if (imageAttachment != null && imageAttachment.preview.isNotEmpty()) {
LaunchedEffect(imageAttachment?.id) {
if (imageAttachment != null) {
withContext(Dispatchers.IO) {
try {
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
val blurhash = if (imageAttachment.preview.contains("::")) {
imageAttachment.preview.split("::").lastOrNull() ?: ""
// Пробуем сначала из blob
if (imageAttachment.blob.isNotEmpty()) {
val decoded = try {
val cleanBase64 = if (imageAttachment.blob.contains(",")) {
imageAttachment.blob.substringAfter(",")
} else {
imageAttachment.preview
imageAttachment.blob
}
if (blurhash.isNotEmpty() && !blurhash.startsWith("http")) {
previewBitmap = BlurHash.decode(blurhash, 40, 40)
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
} catch (e: Exception) {
null
}
if (decoded != null) {
imageBitmap = decoded
return@withContext
}
}
// Если blob нет - загружаем из локального файла
val localBlob = AttachmentFileManager.readAttachment(
context,
imageAttachment.id,
replyData.senderPublicKey,
replyData.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
}
imageBitmap = decoded
}
} catch (e: Exception) {
// Ignore blurhash decode errors
Log.e("ReplyBubble", "Failed to load image: ${e.message}")
}
}
}
@@ -696,9 +735,9 @@ fun ReplyBubble(
.clip(RoundedCornerShape(4.dp))
.background(Color.Gray.copy(alpha = 0.3f))
) {
if (previewBitmap != null) {
if (imageBitmap != null) {
Image(
bitmap = previewBitmap!!.asImageBitmap(),
bitmap = imageBitmap!!.asImageBitmap(),
contentDescription = "Photo preview",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop

View File

@@ -23,7 +23,9 @@ data class ReplyData(
val text: String,
val isFromMe: Boolean,
val isForwarded: Boolean = false,
val attachments: List<MessageAttachment> = emptyList() // 🖼️ Для превью фото в reply
val attachments: List<MessageAttachment> = emptyList(), // 🖼️ Для превью фото в reply
val senderPublicKey: String = "", // Для расшифровки attachments в reply
val recipientPrivateKey: String = "" // Для расшифровки attachments в reply
)
/** Legacy message model (for compatibility) */