feat: Add reply functionality with message quoting in ChatViewModel and ChatDetailScreen

This commit is contained in:
k1ngsterr1
2026-01-13 04:38:38 +05:00
parent 98835c0ae0
commit af10005879
2 changed files with 152 additions and 2 deletions

View File

@@ -119,6 +119,14 @@ private val TelegramSendIcon: ImageVector
}
.build()
/** Данные цитируемого сообщения */
data class ReplyData(
val messageId: String,
val senderName: String, // Имя отправителя цитируемого сообщения
val text: String,
val isFromMe: Boolean // Цитируемое сообщение от меня?
)
/** Модель сообщения (Legacy - для совместимости) */
data class ChatMessage(
val id: String,
@@ -126,7 +134,8 @@ data class ChatMessage(
val isOutgoing: Boolean,
val timestamp: Date,
val status: MessageStatus = MessageStatus.SENT,
val showDateHeader: Boolean = false // Показывать ли разделитель даты
val showDateHeader: Boolean = false, // Показывать ли разделитель даты
val replyData: ReplyData? = null // Данные цитируемого сообщения
)
enum class MessageStatus {
@@ -1434,6 +1443,16 @@ private fun MessageBubble(
.padding(horizontal = 12.dp, vertical = 7.dp)
) {
Column {
// 🔥 Reply bubble (цитата) - как в React Native
message.replyData?.let { reply ->
ReplyBubble(
replyData = reply,
isOutgoing = message.isOutgoing,
isDarkTheme = isDarkTheme
)
Spacer(modifier = Modifier.height(6.dp))
}
AppleEmojiText(text = message.text, color = textColor, fontSize = 16.sp)
Spacer(modifier = Modifier.height(2.dp))
Row(
@@ -1517,6 +1536,83 @@ private fun AnimatedMessageStatus(
}
}
/**
* 🔥 Reply bubble (цитата) внутри сообщения - как в React Native
* Стиль: вертикальная линия слева + имя + текст
*/
@Composable
private fun ReplyBubble(
replyData: ReplyData,
isOutgoing: Boolean,
isDarkTheme: Boolean
) {
// Цвета как в React Native
val backgroundColor = if (isOutgoing) {
Color.Black.copy(alpha = 0.15f)
} else {
Color.Black.copy(alpha = 0.08f)
}
val borderColor = if (isOutgoing) {
Color.White
} else {
PrimaryBlue
}
val nameColor = if (isOutgoing) {
Color.White
} else {
PrimaryBlue
}
val textColor = if (isOutgoing) {
Color.White.copy(alpha = 0.85f)
} else {
if (isDarkTheme) Color.White else Color.Black
}
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.clip(RoundedCornerShape(4.dp))
.background(backgroundColor)
) {
// Вертикальная линия слева (как borderLeft в React Native)
Box(
modifier = Modifier
.width(3.dp)
.fillMaxHeight()
.background(borderColor)
)
// Контент
Column(
modifier = Modifier
.padding(start = 10.dp, end = 10.dp, top = 6.dp, bottom = 6.dp)
) {
// Имя отправителя цитируемого сообщения
Text(
text = replyData.senderName,
color = nameColor,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Текст цитируемого сообщения
Text(
text = replyData.text.ifEmpty { "..." },
color = textColor,
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
/** 🚀 Разделитель даты с fade-in анимацией */
@Composable
private fun DateHeader(dateText: String, secondaryTextColor: Color) {

View File

@@ -12,6 +12,8 @@ import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.json.JSONArray
import org.json.JSONObject
import java.util.UUID
import java.util.Date
import java.util.concurrent.ConcurrentHashMap
@@ -419,6 +421,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* 🔥 Быстрая конвертация Entity -> ChatMessage
*/
private fun entityToChatMessage(entity: MessageEntity): ChatMessage {
// Парсим attachments для поиска MESSAGES (цитата)
val replyData = parseReplyFromAttachments(entity.attachments, entity.fromMe == 1)
return ChatMessage(
id = entity.messageId,
text = entity.plainMessage, // Уже расшифровано при сохранении
@@ -430,10 +435,59 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
2 -> MessageStatus.SENT // Changed from ERROR to SENT
3 -> MessageStatus.READ
else -> MessageStatus.SENT
}
},
replyData = replyData
)
}
/**
* Парсинг MESSAGES attachment для извлечения данных цитаты
* Формат: [{"message_id": "...", "publicKey": "...", "message": "..."}]
*/
private fun parseReplyFromAttachments(attachmentsJson: String, isFromMe: Boolean): ReplyData? {
if (attachmentsJson.isEmpty() || attachmentsJson == "[]") return null
return try {
val attachments = JSONArray(attachmentsJson)
for (i in 0 until attachments.length()) {
val attachment = attachments.getJSONObject(i)
val type = attachment.optInt("type", 0)
// MESSAGES = 1 (цитата)
if (type == 1) {
// Данные могут быть в blob или preview
val dataJson = attachment.optString("blob", "").ifEmpty {
attachment.optString("preview", "")
}
if (dataJson.isEmpty()) continue
val messagesArray = JSONArray(dataJson)
if (messagesArray.length() > 0) {
val replyMessage = messagesArray.getJSONObject(0)
val replyPublicKey = replyMessage.optString("publicKey", "")
val replyText = replyMessage.optString("message", "")
val replyMessageId = replyMessage.optString("message_id", "")
// Определяем, кто автор цитируемого сообщения
// Если publicKey == myPublicKey - цитата от меня
val isReplyFromMe = replyPublicKey == myPublicKey
return ReplyData(
messageId = replyMessageId,
senderName = if (isReplyFromMe) "You" else opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } },
text = replyText,
isFromMe = isReplyFromMe
)
}
}
}
null
} catch (e: Exception) {
Log.e(TAG, "Error parsing reply from attachments", e)
null
}
}
/**
* Получить ключ диалога для группировки сообщений
*/