feat: implement support for multiple forwarded messages in chat components
This commit is contained in:
@@ -979,8 +979,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Парсим attachments для поиска MESSAGES (цитата)
|
// Парсим attachments для поиска MESSAGES (цитата)
|
||||||
// <EFBFBD> P2.3: Передаём plainKeyAndNonce чтобы избежать повторного ECDH
|
// 🚀 P2.3: Передаём plainKeyAndNonce чтобы избежать повторного ECDH
|
||||||
var replyData =
|
val parsedReplyResult =
|
||||||
parseReplyFromAttachments(
|
parseReplyFromAttachments(
|
||||||
attachmentsJson = entity.attachments,
|
attachmentsJson = entity.attachments,
|
||||||
isFromMe = entity.fromMe == 1,
|
isFromMe = entity.fromMe == 1,
|
||||||
@@ -988,6 +988,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
chachaKey = entity.chachaKey,
|
chachaKey = entity.chachaKey,
|
||||||
plainKeyAndNonce = plainKeyAndNonce
|
plainKeyAndNonce = plainKeyAndNonce
|
||||||
)
|
)
|
||||||
|
var replyData = parsedReplyResult?.replyData
|
||||||
|
val forwardedMessages = parsedReplyResult?.forwardedMessages ?: emptyList()
|
||||||
|
|
||||||
// Если не нашли reply в attachments, пробуем распарсить из текста
|
// Если не нашли reply в attachments, пробуем распарсить из текста
|
||||||
if (replyData == null) {
|
if (replyData == null) {
|
||||||
@@ -1014,7 +1016,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
3 -> MessageStatus.READ
|
3 -> MessageStatus.READ
|
||||||
else -> MessageStatus.SENT
|
else -> MessageStatus.SENT
|
||||||
},
|
},
|
||||||
replyData = replyData,
|
replyData = if (forwardedMessages.isNotEmpty()) null else replyData,
|
||||||
|
forwardedMessages = forwardedMessages,
|
||||||
attachments = parsedAttachments,
|
attachments = parsedAttachments,
|
||||||
chachaKey = entity.chachaKey
|
chachaKey = entity.chachaKey
|
||||||
)
|
)
|
||||||
@@ -1118,6 +1121,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* "publicKey": "...", "message": "..."}] 🔥 ВАЖНО: Если blob зашифрован (формат
|
* "publicKey": "...", "message": "..."}] 🔥 ВАЖНО: Если blob зашифрован (формат
|
||||||
* "iv:ciphertext"), расшифровываем его
|
* "iv:ciphertext"), расшифровываем его
|
||||||
*/
|
*/
|
||||||
|
/** Result of parsing reply/forward attachments */
|
||||||
|
data class ParsedReplyResult(
|
||||||
|
val replyData: ReplyData? = null,
|
||||||
|
val forwardedMessages: List<ReplyData> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun parseReplyFromAttachments(
|
private suspend fun parseReplyFromAttachments(
|
||||||
attachmentsJson: String,
|
attachmentsJson: String,
|
||||||
isFromMe: Boolean,
|
isFromMe: Boolean,
|
||||||
@@ -1125,7 +1134,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
chachaKey: String,
|
chachaKey: String,
|
||||||
plainKeyAndNonce: ByteArray? =
|
plainKeyAndNonce: ByteArray? =
|
||||||
null // 🚀 P2.3: Переиспользуем ключ из основной расшифровки
|
null // 🚀 P2.3: Переиспользуем ключ из основной расшифровки
|
||||||
): ReplyData? {
|
): ParsedReplyResult? {
|
||||||
|
|
||||||
if (attachmentsJson.isEmpty() || attachmentsJson == "[]") {
|
if (attachmentsJson.isEmpty() || attachmentsJson == "[]") {
|
||||||
return null
|
return null
|
||||||
@@ -1245,6 +1254,73 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (messagesArray.length() > 0) {
|
if (messagesArray.length() > 0) {
|
||||||
|
val account = myPublicKey ?: return null
|
||||||
|
val dialogKey = getDialogKey(account, opponentKey ?: "")
|
||||||
|
|
||||||
|
// Check if this is a forwarded set or a regular reply
|
||||||
|
// Desktop doesn't set "forwarded" flag, but sends multiple messages in the array
|
||||||
|
val firstMsg = messagesArray.getJSONObject(0)
|
||||||
|
val isForwardedSet = firstMsg.optBoolean("forwarded", false) || messagesArray.length() > 1
|
||||||
|
|
||||||
|
if (isForwardedSet) {
|
||||||
|
// 🔥 Parse ALL forwarded messages (desktop parity)
|
||||||
|
val forwardedList = mutableListOf<ReplyData>()
|
||||||
|
for (idx in 0 until messagesArray.length()) {
|
||||||
|
val fwdMsg = messagesArray.getJSONObject(idx)
|
||||||
|
val fwdPublicKey = fwdMsg.optString("publicKey", "")
|
||||||
|
val fwdText = fwdMsg.optString("message", "")
|
||||||
|
val fwdMessageId = fwdMsg.optString("message_id", "")
|
||||||
|
val fwdTimestamp = fwdMsg.optLong("timestamp", 0L)
|
||||||
|
val fwdSenderName = fwdMsg.optString("senderName", "")
|
||||||
|
val fwdIsFromMe = fwdPublicKey == myPublicKey
|
||||||
|
|
||||||
|
// Parse attachments from this forwarded message
|
||||||
|
val fwdAttachments = mutableListOf<MessageAttachment>()
|
||||||
|
try {
|
||||||
|
val attArray = fwdMsg.optJSONArray("attachments")
|
||||||
|
if (attArray != null) {
|
||||||
|
for (j in 0 until attArray.length()) {
|
||||||
|
val attJson = attArray.getJSONObject(j)
|
||||||
|
val attId = attJson.optString("id", "")
|
||||||
|
val attType = AttachmentType.fromInt(attJson.optInt("type", 0))
|
||||||
|
val attPreview = attJson.optString("preview", "")
|
||||||
|
val attBlob = attJson.optString("blob", "")
|
||||||
|
val attWidth = attJson.optInt("width", 0)
|
||||||
|
val attHeight = attJson.optInt("height", 0)
|
||||||
|
if (attId.isNotEmpty()) {
|
||||||
|
fwdAttachments.add(MessageAttachment(
|
||||||
|
id = attId, type = attType, preview = attPreview,
|
||||||
|
blob = attBlob, width = attWidth, height = attHeight
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
|
val senderDisplayName = fwdSenderName.ifEmpty {
|
||||||
|
if (fwdIsFromMe) "You"
|
||||||
|
else opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardedList.add(ReplyData(
|
||||||
|
messageId = fwdMessageId,
|
||||||
|
senderName = senderDisplayName,
|
||||||
|
text = fwdText,
|
||||||
|
isFromMe = fwdIsFromMe,
|
||||||
|
isForwarded = true,
|
||||||
|
forwardedFromName = senderDisplayName,
|
||||||
|
attachments = fwdAttachments,
|
||||||
|
senderPublicKey = if (fwdIsFromMe) myPublicKey ?: "" else opponentKey ?: "",
|
||||||
|
recipientPrivateKey = myPrivateKey ?: ""
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return ParsedReplyResult(
|
||||||
|
replyData = forwardedList.firstOrNull(),
|
||||||
|
forwardedMessages = forwardedList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular reply (not forwarded) — use first message only
|
||||||
val replyMessage = messagesArray.getJSONObject(0)
|
val replyMessage = messagesArray.getJSONObject(0)
|
||||||
val replyPublicKey = replyMessage.optString("publicKey", "")
|
val replyPublicKey = replyMessage.optString("publicKey", "")
|
||||||
val replyText = replyMessage.optString("message", "")
|
val replyText = replyMessage.optString("message", "")
|
||||||
@@ -1286,8 +1362,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
// 🔥 ВАЖНО: message_id из JSON может не совпадать с messageId в Android БД!
|
// 🔥 ВАЖНО: message_id из JSON может не совпадать с messageId в Android БД!
|
||||||
// Пытаемся найти реальный messageId в текущих сообщениях по тексту и
|
// Пытаемся найти реальный messageId в текущих сообщениях по тексту и
|
||||||
// timestamp
|
// timestamp
|
||||||
val account = myPublicKey ?: return null
|
|
||||||
val dialogKey = getDialogKey(account, opponentKey ?: "")
|
|
||||||
val realMessageId =
|
val realMessageId =
|
||||||
try {
|
try {
|
||||||
// Ищем сообщение в БД по публичному ключу, тексту и timestamp
|
// Ищем сообщение в БД по публичному ключу, тексту и timestamp
|
||||||
@@ -1358,7 +1432,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
else opponentKey ?: "",
|
else opponentKey ?: "",
|
||||||
recipientPrivateKey = myPrivateKey ?: ""
|
recipientPrivateKey = myPrivateKey ?: ""
|
||||||
)
|
)
|
||||||
return result
|
return ParsedReplyResult(replyData = result)
|
||||||
} else {}
|
} else {}
|
||||||
} else {}
|
} else {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -944,9 +944,9 @@ fun ChatsListScreen(
|
|||||||
colors =
|
colors =
|
||||||
TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
Color(0xFF0D8CF4),
|
if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
||||||
scrolledContainerColor =
|
scrolledContainerColor =
|
||||||
Color(0xFF0D8CF4),
|
if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
||||||
navigationIconContentColor =
|
navigationIconContentColor =
|
||||||
Color.White,
|
Color.White,
|
||||||
titleContentColor =
|
titleContentColor =
|
||||||
|
|||||||
@@ -477,6 +477,7 @@ fun MessageBubble(
|
|||||||
message.attachments.isNotEmpty() &&
|
message.attachments.isNotEmpty() &&
|
||||||
message.text.isEmpty() &&
|
message.text.isEmpty() &&
|
||||||
message.replyData == null &&
|
message.replyData == null &&
|
||||||
|
message.forwardedMessages.isEmpty() &&
|
||||||
message.attachments.all {
|
message.attachments.all {
|
||||||
it.type ==
|
it.type ==
|
||||||
com.rosetta.messenger.network.AttachmentType
|
com.rosetta.messenger.network.AttachmentType
|
||||||
@@ -488,6 +489,7 @@ fun MessageBubble(
|
|||||||
message.attachments.isNotEmpty() &&
|
message.attachments.isNotEmpty() &&
|
||||||
message.text.isNotEmpty() &&
|
message.text.isNotEmpty() &&
|
||||||
message.replyData == null &&
|
message.replyData == null &&
|
||||||
|
message.forwardedMessages.isEmpty() &&
|
||||||
message.attachments.all {
|
message.attachments.all {
|
||||||
it.type ==
|
it.type ==
|
||||||
com.rosetta.messenger.network.AttachmentType
|
com.rosetta.messenger.network.AttachmentType
|
||||||
@@ -639,6 +641,19 @@ fun MessageBubble(
|
|||||||
.padding(bubblePadding)
|
.padding(bubblePadding)
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
// 🔥 Forwarded messages (multiple, desktop parity)
|
||||||
|
if (message.forwardedMessages.isNotEmpty()) {
|
||||||
|
ForwardedMessagesBubble(
|
||||||
|
forwardedMessages = message.forwardedMessages,
|
||||||
|
isOutgoing = message.isOutgoing,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
chachaKey = message.chachaKey,
|
||||||
|
privateKey = privateKey,
|
||||||
|
onImageClick = onImageClick
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
message.replyData?.let { reply ->
|
message.replyData?.let { reply ->
|
||||||
ReplyBubble(
|
ReplyBubble(
|
||||||
replyData = reply,
|
replyData = reply,
|
||||||
@@ -793,8 +808,8 @@ fun MessageBubble(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если есть reply - Telegram-style layout
|
// Если есть reply/forward - Telegram-style layout
|
||||||
if (message.replyData != null && message.text.isNotEmpty()
|
if ((message.replyData != null || message.forwardedMessages.isNotEmpty()) && message.text.isNotEmpty()
|
||||||
) {
|
) {
|
||||||
TelegramStyleMessageContent(
|
TelegramStyleMessageContent(
|
||||||
textContent = {
|
textContent = {
|
||||||
@@ -1408,6 +1423,239 @@ fun ReplyBubble(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Forwarded messages bubble — renders ALL forwarded messages like Desktop (Telegram-style) */
|
||||||
|
@Composable
|
||||||
|
fun ForwardedMessagesBubble(
|
||||||
|
forwardedMessages: List<ReplyData>,
|
||||||
|
isOutgoing: Boolean,
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
chachaKey: String = "",
|
||||||
|
privateKey: String = "",
|
||||||
|
onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> }
|
||||||
|
) {
|
||||||
|
val backgroundColor =
|
||||||
|
if (isOutgoing) Color.Black.copy(alpha = 0.1f)
|
||||||
|
else Color.Black.copy(alpha = 0.05f)
|
||||||
|
val borderColor = if (isOutgoing) Color.White.copy(alpha = 0.6f) else PrimaryBlue
|
||||||
|
|
||||||
|
// Color palette for different senders
|
||||||
|
val senderColors = listOf(
|
||||||
|
Color(0xFF3390EC), // blue
|
||||||
|
Color(0xFF4CAF50), // green
|
||||||
|
Color(0xFFE65100), // orange
|
||||||
|
Color(0xFF9C27B0), // purple
|
||||||
|
Color(0xFFE53935), // red
|
||||||
|
Color(0xFF00ACC1), // cyan
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map sender names to consistent colors
|
||||||
|
val senderColorMap = remember(forwardedMessages) {
|
||||||
|
val uniqueSenders = forwardedMessages.map { it.forwardedFromName }.distinct()
|
||||||
|
uniqueSenders.mapIndexed { index, name ->
|
||||||
|
name to senderColors[index % senderColors.size]
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(backgroundColor)
|
||||||
|
) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)) {
|
||||||
|
// Single vertical border line for entire block
|
||||||
|
Box(modifier = Modifier.width(3.dp).fillMaxHeight().background(borderColor))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 8.dp, end = 8.dp, top = 4.dp, bottom = 4.dp)
|
||||||
|
) {
|
||||||
|
forwardedMessages.forEachIndexed { index, fwd ->
|
||||||
|
if (index > 0) {
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Divider(
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.1f)
|
||||||
|
else Color.Black.copy(alpha = 0.08f),
|
||||||
|
thickness = 0.5.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
val nameColor = if (isOutgoing) {
|
||||||
|
Color.White
|
||||||
|
} else {
|
||||||
|
senderColorMap[fwd.forwardedFromName] ?: PrimaryBlue
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Forwarded from [Name]"
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "Forwarded from ",
|
||||||
|
color = nameColor.copy(alpha = 0.7f),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = fwd.forwardedFromName.ifEmpty { "User" },
|
||||||
|
color = nameColor,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message text
|
||||||
|
if (fwd.text.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
val textColor = if (isOutgoing) Color.White.copy(alpha = 0.9f)
|
||||||
|
else if (isDarkTheme) Color.White else Color.Black
|
||||||
|
AppleEmojiText(
|
||||||
|
text = fwd.text,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
maxLines = 50,
|
||||||
|
overflow = android.text.TextUtils.TruncateAt.END,
|
||||||
|
enableLinks = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachments (images)
|
||||||
|
val imageAttachments = fwd.attachments.filter { it.type == AttachmentType.IMAGE }
|
||||||
|
if (imageAttachments.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
imageAttachments.forEach { imgAtt ->
|
||||||
|
ForwardedImagePreview(
|
||||||
|
attachment = imgAtt,
|
||||||
|
senderPublicKey = fwd.senderPublicKey,
|
||||||
|
recipientPrivateKey = fwd.recipientPrivateKey,
|
||||||
|
chachaKey = chachaKey,
|
||||||
|
privateKey = privateKey,
|
||||||
|
onImageClick = onImageClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Image preview inside a forwarded message */
|
||||||
|
@Composable
|
||||||
|
private fun ForwardedImagePreview(
|
||||||
|
attachment: MessageAttachment,
|
||||||
|
senderPublicKey: String,
|
||||||
|
recipientPrivateKey: String,
|
||||||
|
chachaKey: String,
|
||||||
|
privateKey: String,
|
||||||
|
onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit
|
||||||
|
) {
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var blurPreviewBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
|
// Load blur preview first
|
||||||
|
LaunchedEffect(attachment.id) {
|
||||||
|
if (attachment.preview.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val bitmap = BlurHash.decode(attachment.preview, 32, 32)
|
||||||
|
if (bitmap != null) blurPreviewBitmap = bitmap
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt and load full image
|
||||||
|
LaunchedEffect(attachment.id) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// Try loading from file cache first
|
||||||
|
val cached = AttachmentFileManager.readAttachment(
|
||||||
|
context, attachment.id, senderPublicKey, recipientPrivateKey
|
||||||
|
)
|
||||||
|
if (cached != null) {
|
||||||
|
val bytes = Base64.decode(cached, Base64.DEFAULT)
|
||||||
|
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.let {
|
||||||
|
imageBitmap = it
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try decrypting from blob
|
||||||
|
if (attachment.blob.isNotEmpty()) {
|
||||||
|
val keyToUse = chachaKey.ifEmpty { recipientPrivateKey }
|
||||||
|
val privKey = privateKey.ifEmpty { recipientPrivateKey }
|
||||||
|
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
||||||
|
attachment.blob, keyToUse, privKey
|
||||||
|
)
|
||||||
|
if (decrypted != null) {
|
||||||
|
val bytes = Base64.decode(decrypted, Base64.DEFAULT)
|
||||||
|
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.let {
|
||||||
|
imageBitmap = it
|
||||||
|
ImageBitmapCache.put("img_${attachment.id}", it)
|
||||||
|
AttachmentFileManager.saveAttachment(
|
||||||
|
context, decrypted, attachment.id,
|
||||||
|
senderPublicKey, recipientPrivateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imgWidth = attachment.width.takeIf { it > 0 } ?: 300
|
||||||
|
val imgHeight = attachment.height.takeIf { it > 0 } ?: 200
|
||||||
|
val aspectRatio = (imgWidth.toFloat() / imgHeight.toFloat()).coerceIn(0.5f, 2.5f)
|
||||||
|
|
||||||
|
var photoBoxBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(aspectRatio)
|
||||||
|
.heightIn(max = 200.dp)
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(Color.Gray.copy(alpha = 0.2f))
|
||||||
|
.onGloballyPositioned { coords ->
|
||||||
|
val bounds = coords.boundsInWindow()
|
||||||
|
photoBoxBounds = ImageSourceBounds(
|
||||||
|
left = bounds.left, top = bounds.top,
|
||||||
|
width = bounds.width, height = bounds.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable { onImageClick(attachment.id, photoBoxBounds) }
|
||||||
|
) {
|
||||||
|
if (imageBitmap != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = imageBitmap!!.asImageBitmap(),
|
||||||
|
contentDescription = "Photo",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else if (blurPreviewBitmap != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = blurPreviewBitmap!!.asImageBitmap(),
|
||||||
|
contentDescription = "Photo",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
TablerIcons.Photo,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Message skeleton loader with shimmer animation */
|
/** Message skeleton loader with shimmer animation */
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageSkeletonList(isDarkTheme: Boolean, modifier: Modifier = Modifier) {
|
fun MessageSkeletonList(isDarkTheme: Boolean, modifier: Modifier = Modifier) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ data class ChatMessage(
|
|||||||
val status: MessageStatus = MessageStatus.SENT,
|
val status: MessageStatus = MessageStatus.SENT,
|
||||||
val showDateHeader: Boolean = false,
|
val showDateHeader: Boolean = false,
|
||||||
val replyData: ReplyData? = null,
|
val replyData: ReplyData? = null,
|
||||||
|
val forwardedMessages: List<ReplyData> = emptyList(), // Multiple forwarded messages (desktop parity)
|
||||||
val attachments: List<MessageAttachment> = emptyList(),
|
val attachments: List<MessageAttachment> = emptyList(),
|
||||||
val chachaKey: String = "" // Для расшифровки attachments
|
val chachaKey: String = "" // Для расшифровки attachments
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user