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

View File

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

View File

@@ -23,7 +23,9 @@ data class ReplyData(
val text: String, val text: String,
val isFromMe: Boolean, val isFromMe: Boolean,
val isForwarded: Boolean = false, 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) */ /** Legacy message model (for compatibility) */