feat: Enhance message selection and forwarding logic, ensuring avatar messages are excluded
This commit is contained in:
@@ -379,6 +379,35 @@ fun ChatDetailScreen(
|
|||||||
val hasReply = replyMessages.isNotEmpty()
|
val hasReply = replyMessages.isNotEmpty()
|
||||||
val isForwardMode by viewModel.isForwardMode.collectAsState()
|
val isForwardMode by viewModel.isForwardMode.collectAsState()
|
||||||
|
|
||||||
|
// Avatar-сообщения не должны попадать в selection ни при каких условиях.
|
||||||
|
val avatarMessageIds =
|
||||||
|
remember(messages) {
|
||||||
|
messages.filter { msg ->
|
||||||
|
msg.attachments.any { it.type == AttachmentType.AVATAR }
|
||||||
|
}.map { it.id }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(avatarMessageIds) {
|
||||||
|
if (selectedMessages.isNotEmpty()) {
|
||||||
|
val filteredSelection = selectedMessages - avatarMessageIds
|
||||||
|
if (filteredSelection != selectedMessages) {
|
||||||
|
selectedMessages = filteredSelection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val toggleMessageSelection: (messageId: String, canSelect: Boolean) -> Unit =
|
||||||
|
{ messageId, canSelect ->
|
||||||
|
if (canSelect) {
|
||||||
|
selectedMessages =
|
||||||
|
if (selectedMessages.contains(messageId)) {
|
||||||
|
selectedMessages - messageId
|
||||||
|
} else {
|
||||||
|
selectedMessages + messageId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔥 ПАГИНАЦИЯ: Загружаем старые сообщения при прокрутке вверх
|
// 🔥 ПАГИНАЦИЯ: Загружаем старые сообщения при прокрутке вверх
|
||||||
// NOTE: Не нужен ручной scrollToItem - LazyColumn с reverseLayout=true
|
// NOTE: Не нужен ручной scrollToItem - LazyColumn с reverseLayout=true
|
||||||
// автоматически сохраняет позицию благодаря стабильным ключам (key = message.id)
|
// автоматически сохраняет позицию благодаря стабильным ключам (key = message.id)
|
||||||
@@ -661,9 +690,10 @@ fun ChatDetailScreen(
|
|||||||
messages
|
messages
|
||||||
.filter {
|
.filter {
|
||||||
selectedMessages
|
selectedMessages
|
||||||
.contains(
|
.contains(it.id) &&
|
||||||
it.id
|
it.attachments.none { attachment ->
|
||||||
)
|
attachment.type == AttachmentType.AVATAR
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy<ChatMessage>(
|
compareBy<ChatMessage>(
|
||||||
@@ -718,9 +748,10 @@ fun ChatDetailScreen(
|
|||||||
messages
|
messages
|
||||||
.filter {
|
.filter {
|
||||||
selectedMessages
|
selectedMessages
|
||||||
.contains(
|
.contains(it.id) &&
|
||||||
it.id
|
it.attachments.none { attachment ->
|
||||||
)
|
attachment.type == AttachmentType.AVATAR
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.forEach {
|
.forEach {
|
||||||
msg
|
msg
|
||||||
@@ -1216,8 +1247,9 @@ fun ChatDetailScreen(
|
|||||||
label = "bottomBarContent"
|
label = "bottomBarContent"
|
||||||
) { selectionMode ->
|
) { selectionMode ->
|
||||||
if (selectionMode) {
|
if (selectionMode) {
|
||||||
// SELECTION ACTION BAR - Reply/Forward
|
if (!isSystemAccount) {
|
||||||
Column(
|
// SELECTION ACTION BAR - Reply/Forward
|
||||||
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.background(
|
.background(
|
||||||
@@ -1320,10 +1352,11 @@ fun ChatDetailScreen(
|
|||||||
val selectedMsgs =
|
val selectedMsgs =
|
||||||
messages
|
messages
|
||||||
.filter {
|
.filter {
|
||||||
selectedMessages
|
selectedMessages
|
||||||
.contains(
|
.contains(it.id) &&
|
||||||
it.id
|
it.attachments.none { attachment ->
|
||||||
)
|
attachment.type == AttachmentType.AVATAR
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy<ChatMessage>(
|
compareBy<ChatMessage>(
|
||||||
@@ -1406,10 +1439,11 @@ fun ChatDetailScreen(
|
|||||||
val selectedMsgs =
|
val selectedMsgs =
|
||||||
messages
|
messages
|
||||||
.filter {
|
.filter {
|
||||||
selectedMessages
|
selectedMessages
|
||||||
.contains(
|
.contains(it.id) &&
|
||||||
it.id
|
it.attachments.none { attachment ->
|
||||||
)
|
attachment.type == AttachmentType.AVATAR
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy<ChatMessage>(
|
compareBy<ChatMessage>(
|
||||||
@@ -1556,6 +1590,7 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (!isSystemAccount) {
|
} else if (!isSystemAccount) {
|
||||||
// INPUT BAR
|
// INPUT BAR
|
||||||
Column {
|
Column {
|
||||||
@@ -1972,18 +2007,10 @@ fun ChatDetailScreen(
|
|||||||
showEmojiPicker =
|
showEmojiPicker =
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
selectedMessages =
|
toggleMessageSelection(
|
||||||
if (selectedMessages
|
selectionKey,
|
||||||
.contains(
|
true
|
||||||
selectionKey
|
)
|
||||||
)
|
|
||||||
) {
|
|
||||||
selectedMessages -
|
|
||||||
selectionKey
|
|
||||||
} else {
|
|
||||||
selectedMessages +
|
|
||||||
selectionKey
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
val hasAvatar =
|
val hasAvatar =
|
||||||
@@ -1993,20 +2020,11 @@ fun ChatDetailScreen(
|
|||||||
AttachmentType
|
AttachmentType
|
||||||
.AVATAR
|
.AVATAR
|
||||||
}
|
}
|
||||||
if (isSelectionMode && !hasAvatar
|
if (isSelectionMode) {
|
||||||
) {
|
toggleMessageSelection(
|
||||||
selectedMessages =
|
selectionKey,
|
||||||
if (selectedMessages
|
!hasAvatar
|
||||||
.contains(
|
)
|
||||||
selectionKey
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
selectedMessages -
|
|
||||||
selectionKey
|
|
||||||
} else {
|
|
||||||
selectedMessages +
|
|
||||||
selectionKey
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSwipeToReply = {
|
onSwipeToReply = {
|
||||||
@@ -2018,7 +2036,8 @@ fun ChatDetailScreen(
|
|||||||
AttachmentType
|
AttachmentType
|
||||||
.AVATAR
|
.AVATAR
|
||||||
}
|
}
|
||||||
if (!hasAvatar
|
if (!hasAvatar &&
|
||||||
|
!isSystemAccount
|
||||||
) {
|
) {
|
||||||
viewModel
|
viewModel
|
||||||
.setReplyMessages(
|
.setReplyMessages(
|
||||||
@@ -2364,33 +2383,37 @@ fun ChatDetailScreen(
|
|||||||
},
|
},
|
||||||
onChatsSelected = { selectedDialogs ->
|
onChatsSelected = { selectedDialogs ->
|
||||||
showForwardPicker = false
|
showForwardPicker = false
|
||||||
if (selectedDialogs.isNotEmpty()) {
|
if (selectedDialogs.isEmpty()) {
|
||||||
val primaryDialog = selectedDialogs.first()
|
ForwardManager.clear()
|
||||||
val additionalDialogs = selectedDialogs.drop(1)
|
return@ForwardChatPickerBottomSheet
|
||||||
|
}
|
||||||
|
|
||||||
// Отправляем forward напрямую в дополнительные чаты
|
val forwardMessages = ForwardManager.consumeForwardMessages()
|
||||||
val fwdMessages = ForwardManager.consumeForwardMessages()
|
ForwardManager.clear()
|
||||||
if (additionalDialogs.isNotEmpty() && fwdMessages.isNotEmpty()) {
|
if (forwardMessages.isEmpty()) {
|
||||||
additionalDialogs.forEach { dialog ->
|
return@ForwardChatPickerBottomSheet
|
||||||
viewModel.sendForwardDirectly(
|
}
|
||||||
dialog.opponentKey,
|
|
||||||
fwdMessages
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Навигируемся в первый выбранный чат с forward
|
// Реальная отправка forward во все выбранные чаты.
|
||||||
ForwardManager.setForwardMessages(fwdMessages, showPicker = false)
|
selectedDialogs.forEach { dialog ->
|
||||||
ForwardManager.selectChat(primaryDialog.opponentKey)
|
viewModel.sendForwardDirectly(
|
||||||
val searchUser =
|
dialog.opponentKey,
|
||||||
SearchUser(
|
forwardMessages
|
||||||
title = primaryDialog.opponentTitle,
|
)
|
||||||
username = primaryDialog.opponentUsername,
|
}
|
||||||
publicKey = primaryDialog.opponentKey,
|
|
||||||
verified = primaryDialog.verified,
|
// Открываем первый выбранный чат (кроме текущего), чтобы сразу увидеть результат.
|
||||||
online = primaryDialog.isOnline
|
val primaryDialog = selectedDialogs.first()
|
||||||
)
|
if (primaryDialog.opponentKey != user.publicKey) {
|
||||||
onNavigateToChat(searchUser)
|
val searchUser =
|
||||||
|
SearchUser(
|
||||||
|
title = primaryDialog.opponentTitle,
|
||||||
|
username = primaryDialog.opponentUsername,
|
||||||
|
publicKey = primaryDialog.opponentKey,
|
||||||
|
verified = primaryDialog.verified,
|
||||||
|
online = primaryDialog.isOnline
|
||||||
|
)
|
||||||
|
onNavigateToChat(searchUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2210,23 +2210,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
isFromMe = true,
|
isFromMe = true,
|
||||||
delivered = if (isSavedMessages) 1 else 0,
|
delivered = if (isSavedMessages) 1 else 0,
|
||||||
attachmentsJson = attachmentsJson
|
attachmentsJson = attachmentsJson,
|
||||||
|
opponentPublicKey = recipientPublicKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// Сохраняем диалог (для списка чатов)
|
// Обновляем диалог (для списка чатов) из таблицы сообщений.
|
||||||
val db = RosettaDatabase.getDatabase(context)
|
val db = RosettaDatabase.getDatabase(context)
|
||||||
val dialogDao = db.dialogDao()
|
val dialogDao = db.dialogDao()
|
||||||
val dialogKey = if (sender < recipientPublicKey) "$sender:$recipientPublicKey"
|
if (isSavedMessages) {
|
||||||
else "$recipientPublicKey:$sender"
|
dialogDao.updateSavedMessagesDialogFromMessages(sender)
|
||||||
val existingDialog = dialogDao.getDialog(sender, recipientPublicKey)
|
} else {
|
||||||
val encryptedLastMsg = CryptoManager.encryptWithPassword("Forwarded message", privateKey)
|
dialogDao.updateDialogFromMessages(sender, recipientPublicKey)
|
||||||
if (existingDialog != null) {
|
|
||||||
dialogDao.updateLastMessage(
|
|
||||||
sender,
|
|
||||||
recipientPublicKey,
|
|
||||||
encryptedLastMsg,
|
|
||||||
timestamp
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { }
|
} catch (e: Exception) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,25 +181,13 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
loadUserInfoForDialog(dialog.opponentKey)
|
loadUserInfoForDialog(dialog.opponentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 Расшифровка теперь кэшируется в CryptoManager!
|
// Безопасная дешифровка превью: никогда не показываем raw ciphertext.
|
||||||
val decryptedLastMessage =
|
val decryptedLastMessage =
|
||||||
try {
|
decryptDialogPreview(
|
||||||
if (privateKey.isNotEmpty() &&
|
encryptedLastMessage =
|
||||||
dialog.lastMessage
|
|
||||||
.isNotEmpty()
|
|
||||||
) {
|
|
||||||
CryptoManager.decryptWithPassword(
|
|
||||||
dialog.lastMessage,
|
dialog.lastMessage,
|
||||||
privateKey
|
privateKey = privateKey
|
||||||
)
|
)
|
||||||
?: dialog.lastMessage
|
|
||||||
} else {
|
|
||||||
dialog.lastMessage
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
dialog.lastMessage // Fallback на
|
|
||||||
// зашифрованный текст
|
|
||||||
}
|
|
||||||
|
|
||||||
// <20> ОПТИМИЗАЦИЯ: Используем денормализованные поля из
|
// <20> ОПТИМИЗАЦИЯ: Используем денормализованные поля из
|
||||||
// DialogEntity
|
// DialogEntity
|
||||||
@@ -211,52 +199,12 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
// 📎 Определяем тип attachment из кэшированного поля в
|
// 📎 Определяем тип attachment из кэшированного поля в
|
||||||
// DialogEntity
|
// DialogEntity
|
||||||
val attachmentType =
|
val attachmentType =
|
||||||
try {
|
resolveAttachmentType(
|
||||||
val attachmentsJson =
|
attachmentsJson =
|
||||||
dialog.lastMessageAttachments
|
dialog.lastMessageAttachments,
|
||||||
if (attachmentsJson.isNotEmpty() &&
|
decryptedLastMessage =
|
||||||
attachmentsJson != "[]"
|
decryptedLastMessage
|
||||||
) {
|
)
|
||||||
val attachments =
|
|
||||||
org.json.JSONArray(
|
|
||||||
attachmentsJson
|
|
||||||
)
|
|
||||||
if (attachments.length() > 0) {
|
|
||||||
val firstAttachment =
|
|
||||||
attachments.getJSONObject(0)
|
|
||||||
val type =
|
|
||||||
firstAttachment.optInt(
|
|
||||||
"type",
|
|
||||||
-1
|
|
||||||
)
|
|
||||||
when (type) {
|
|
||||||
0 ->
|
|
||||||
"Photo" // AttachmentType.IMAGE = 0
|
|
||||||
1 -> {
|
|
||||||
// AttachmentType.MESSAGES =
|
|
||||||
// 1 (Reply или Forward)
|
|
||||||
// Reply: есть текст
|
|
||||||
// сообщения -> показываем
|
|
||||||
// текст (null)
|
|
||||||
// Forward: текст пустой ->
|
|
||||||
// показываем "Forwarded"
|
|
||||||
if (decryptedLastMessage
|
|
||||||
.isNotEmpty()
|
|
||||||
)
|
|
||||||
null
|
|
||||||
else "Forwarded"
|
|
||||||
}
|
|
||||||
2 ->
|
|
||||||
"File" // AttachmentType.FILE = 2
|
|
||||||
3 ->
|
|
||||||
"Avatar" // AttachmentType.AVATAR = 3
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
} else null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
DialogUiModel(
|
DialogUiModel(
|
||||||
id = dialog.id,
|
id = dialog.id,
|
||||||
@@ -336,74 +284,23 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
loadUserInfoForRequest(dialog.opponentKey)
|
loadUserInfoForRequest(dialog.opponentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 Расшифровка теперь кэшируется в CryptoManager!
|
// Безопасная дешифровка превью: никогда не показываем raw ciphertext.
|
||||||
val decryptedLastMessage =
|
val decryptedLastMessage =
|
||||||
try {
|
decryptDialogPreview(
|
||||||
if (privateKey.isNotEmpty() &&
|
encryptedLastMessage =
|
||||||
dialog.lastMessage
|
|
||||||
.isNotEmpty()
|
|
||||||
) {
|
|
||||||
CryptoManager.decryptWithPassword(
|
|
||||||
dialog.lastMessage,
|
dialog.lastMessage,
|
||||||
privateKey
|
privateKey = privateKey
|
||||||
)
|
)
|
||||||
?: dialog.lastMessage
|
|
||||||
} else {
|
|
||||||
dialog.lastMessage
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
dialog.lastMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// 📎 Определяем тип attachment из кэшированного поля в
|
// 📎 Определяем тип attachment из кэшированного поля в
|
||||||
// DialogEntity
|
// DialogEntity
|
||||||
val attachmentType =
|
val attachmentType =
|
||||||
try {
|
resolveAttachmentType(
|
||||||
val attachmentsJson =
|
attachmentsJson =
|
||||||
dialog.lastMessageAttachments
|
dialog.lastMessageAttachments,
|
||||||
if (attachmentsJson.isNotEmpty() &&
|
decryptedLastMessage =
|
||||||
attachmentsJson != "[]"
|
decryptedLastMessage
|
||||||
) {
|
)
|
||||||
val attachments =
|
|
||||||
org.json.JSONArray(
|
|
||||||
attachmentsJson
|
|
||||||
)
|
|
||||||
if (attachments.length() > 0) {
|
|
||||||
val firstAttachment =
|
|
||||||
attachments.getJSONObject(0)
|
|
||||||
val type =
|
|
||||||
firstAttachment.optInt(
|
|
||||||
"type",
|
|
||||||
-1
|
|
||||||
)
|
|
||||||
when (type) {
|
|
||||||
0 ->
|
|
||||||
"Photo" // AttachmentType.IMAGE = 0
|
|
||||||
1 -> {
|
|
||||||
// AttachmentType.MESSAGES =
|
|
||||||
// 1 (Reply или Forward)
|
|
||||||
// Reply: есть текст
|
|
||||||
// сообщения -> показываем
|
|
||||||
// текст (null)
|
|
||||||
// Forward: текст пустой ->
|
|
||||||
// показываем "Forwarded"
|
|
||||||
if (decryptedLastMessage
|
|
||||||
.isNotEmpty()
|
|
||||||
)
|
|
||||||
null
|
|
||||||
else "Forwarded"
|
|
||||||
}
|
|
||||||
2 ->
|
|
||||||
"File" // AttachmentType.FILE = 2
|
|
||||||
3 ->
|
|
||||||
"Avatar" // AttachmentType.AVATAR = 3
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
} else null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
DialogUiModel(
|
DialogUiModel(
|
||||||
id = dialog.id,
|
id = dialog.id,
|
||||||
@@ -493,6 +390,80 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decryptDialogPreview(encryptedLastMessage: String, privateKey: String): String {
|
||||||
|
if (encryptedLastMessage.isEmpty()) return ""
|
||||||
|
|
||||||
|
if (privateKey.isEmpty()) {
|
||||||
|
return if (isLikelyEncryptedPayload(encryptedLastMessage)) "" else encryptedLastMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
val decrypted =
|
||||||
|
try {
|
||||||
|
CryptoManager.decryptWithPassword(encryptedLastMessage, privateKey)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
return when {
|
||||||
|
decrypted != null -> decrypted
|
||||||
|
isLikelyEncryptedPayload(encryptedLastMessage) -> ""
|
||||||
|
else -> encryptedLastMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveAttachmentType(
|
||||||
|
attachmentsJson: String,
|
||||||
|
decryptedLastMessage: String
|
||||||
|
): String? {
|
||||||
|
if (attachmentsJson.isEmpty() || attachmentsJson == "[]") return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val attachments = org.json.JSONArray(attachmentsJson)
|
||||||
|
if (attachments.length() == 0) return null
|
||||||
|
|
||||||
|
val firstAttachment = attachments.getJSONObject(0)
|
||||||
|
when (firstAttachment.optInt("type", -1)) {
|
||||||
|
0 -> "Photo" // AttachmentType.IMAGE = 0
|
||||||
|
1 -> {
|
||||||
|
// AttachmentType.MESSAGES = 1 (Reply/Forward).
|
||||||
|
// Если текст пустой — показываем "Forwarded" как в desktop.
|
||||||
|
if (decryptedLastMessage.isNotEmpty()) null else "Forwarded"
|
||||||
|
}
|
||||||
|
2 -> "File" // AttachmentType.FILE = 2
|
||||||
|
3 -> "Avatar" // AttachmentType.AVATAR = 3
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Fallback: если JSON поврежден, но видно MESSAGES attachment и текста нет — это forward.
|
||||||
|
val hasMessagesType =
|
||||||
|
attachmentsJson.contains("\"type\":1") ||
|
||||||
|
attachmentsJson.contains("\"type\": 1")
|
||||||
|
if (decryptedLastMessage.isEmpty() && hasMessagesType) {
|
||||||
|
"Forwarded"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLikelyEncryptedPayload(value: String): Boolean {
|
||||||
|
if (value.startsWith("CHNK:")) return true
|
||||||
|
|
||||||
|
val parts = value.split(":", limit = 2)
|
||||||
|
if (parts.size != 2) return false
|
||||||
|
|
||||||
|
fun isBase64Like(part: String): Boolean {
|
||||||
|
if (part.length < 12) return false
|
||||||
|
return part.all { ch ->
|
||||||
|
ch.isLetterOrDigit() ||
|
||||||
|
ch == '+' || ch == '/' || ch == '=' ||
|
||||||
|
ch == '-' || ch == '_'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isBase64Like(parts[0]) && isBase64Like(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создать или обновить диалог после отправки/получения сообщения 🔥 Используем
|
* Создать или обновить диалог после отправки/получения сообщения 🔥 Используем
|
||||||
* updateDialogFromMessages для пересчета счетчиков из messages 📁 SAVED MESSAGES: Использует
|
* updateDialogFromMessages для пересчета счетчиков из messages 📁 SAVED MESSAGES: Использует
|
||||||
|
|||||||
@@ -225,9 +225,9 @@ fun ForwardChatPickerBottomSheet(
|
|||||||
onClick = {
|
onClick = {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
selectedChats = if (isSelected) {
|
selectedChats = if (isSelected) {
|
||||||
emptySet()
|
selectedChats - dialog.opponentKey
|
||||||
} else {
|
} else {
|
||||||
setOf(dialog.opponentKey)
|
selectedChats + dialog.opponentKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -275,7 +275,7 @@ fun ForwardChatPickerBottomSheet(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (hasSelection) "Forward" else "Select a chat",
|
text = if (hasSelection) "Forward (${selectedChats.size})" else "Select a chat",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -360,8 +360,8 @@ fun MessageBubble(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth().pointerInput(isSafeSystemMessage) {
|
Modifier.fillMaxWidth().pointerInput(isSystemSafeChat) {
|
||||||
if (isSafeSystemMessage) return@pointerInput
|
if (isSystemSafeChat) return@pointerInput
|
||||||
// 🔥 Простой горизонтальный свайп для reply
|
// 🔥 Простой горизонтальный свайп для reply
|
||||||
// Используем detectHorizontalDragGestures который лучше работает со
|
// Используем detectHorizontalDragGestures который лучше работает со
|
||||||
// скроллом
|
// скроллом
|
||||||
|
|||||||
Reference in New Issue
Block a user