feat: Enhance reply functionality with public and private key support for attachment decryption
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) */
|
||||||
|
|||||||
Reference in New Issue
Block a user