Files
mobile-android/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt

799 lines
33 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.rosetta.messenger.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
// ═══════════════════════════════════════════════════════════
// 📌 PINNED MESSAGES
// ═══════════════════════════════════════════════════════════
/** Entity для закреплённых сообщений в чате (Telegram-style pinned messages) */
@Entity(
tableName = "pinned_messages",
indices =
[
Index(
value = ["account", "dialog_key", "message_id"],
unique = true
),
Index(value = ["account", "dialog_key", "pinned_at"])]
)
data class PinnedMessageEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "account") val account: String,
@ColumnInfo(name = "dialog_key") val dialogKey: String,
@ColumnInfo(name = "message_id") val messageId: String,
@ColumnInfo(name = "pinned_at") val pinnedAt: Long = System.currentTimeMillis()
)
/** DAO для работы с закреплёнными сообщениями */
@Dao
interface PinnedMessageDao {
/** Закрепить сообщение (IGNORE если уже закреплено) */
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertPin(pin: PinnedMessageEntity): Long
/** Открепить конкретное сообщение */
@Query(
"DELETE FROM pinned_messages WHERE account = :account AND dialog_key = :dialogKey AND message_id = :messageId"
)
suspend fun removePin(account: String, dialogKey: String, messageId: String)
/** Получить все закреплённые сообщения диалога (Flow для реактивных обновлений) */
@Query(
"""
SELECT * FROM pinned_messages
WHERE account = :account AND dialog_key = :dialogKey
ORDER BY pinned_at DESC
"""
)
fun getPinnedMessages(account: String, dialogKey: String): Flow<List<PinnedMessageEntity>>
/** Проверить, закреплено ли сообщение */
@Query(
"SELECT EXISTS(SELECT 1 FROM pinned_messages WHERE account = :account AND dialog_key = :dialogKey AND message_id = :messageId)"
)
suspend fun isPinned(account: String, dialogKey: String, messageId: String): Boolean
/** Открепить все сообщения диалога */
@Query("DELETE FROM pinned_messages WHERE account = :account AND dialog_key = :dialogKey")
suspend fun unpinAll(account: String, dialogKey: String): Int
/** Количество закреплённых сообщений в диалоге */
@Query(
"SELECT COUNT(*) FROM pinned_messages WHERE account = :account AND dialog_key = :dialogKey"
)
suspend fun getPinnedCount(account: String, dialogKey: String): Int
}
/** 🔥 Data class для статуса последнего сообщения */
data class LastMessageStatus(
@ColumnInfo(name = "from_me") val fromMe: Int,
@ColumnInfo(name = "delivered") val delivered: Int,
@ColumnInfo(name = "read") val read: Int
)
/** Entity для сообщений - как в React Native версии */
@Entity(
tableName = "messages",
indices =
[
Index(value = ["account", "from_public_key", "to_public_key", "timestamp"]),
Index(value = ["account", "message_id"], unique = true),
Index(value = ["account", "dialog_key", "timestamp"])]
)
data class MessageEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "account") val account: String, // Мой публичный ключ
@ColumnInfo(name = "from_public_key") val fromPublicKey: String, // Отправитель
@ColumnInfo(name = "to_public_key") val toPublicKey: String, // Получатель
@ColumnInfo(name = "content") val content: String, // Зашифрованное содержимое
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
@ColumnInfo(name = "chacha_key") val chachaKey: String, // Зашифрованный ключ
@ColumnInfo(name = "read") val read: Int = 0, // Прочитано (0/1)
@ColumnInfo(name = "from_me") val fromMe: Int = 0, // Мое сообщение (0/1)
@ColumnInfo(name = "delivered")
val delivered: Int = 0, // Статус доставки (0=WAITING, 1=DELIVERED, 2=ERROR)
@ColumnInfo(name = "message_id") val messageId: String, // UUID сообщения
@ColumnInfo(name = "plain_message")
val plainMessage: String, // 🔒 Зашифрованный текст (encryptWithPassword) для хранения в БД
@ColumnInfo(name = "attachments") val attachments: String = "[]", // JSON массив вложений
@ColumnInfo(name = "reply_to_message_id")
val replyToMessageId: String? = null, // ID цитируемого сообщения
@ColumnInfo(name = "dialog_key") val dialogKey: String // Ключ диалога для быстрой выборки
)
/** Entity для диалогов (кэш последнего сообщения) */
@Entity(
tableName = "dialogs",
indices =
[
Index(value = ["account", "opponent_key"], unique = true),
Index(value = ["account", "last_message_timestamp"])]
)
data class DialogEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "account") val account: String, // Мой публичный ключ
@ColumnInfo(name = "opponent_key") val opponentKey: String, // Публичный ключ собеседника
@ColumnInfo(name = "opponent_title") val opponentTitle: String = "", // Имя собеседника
@ColumnInfo(name = "opponent_username")
val opponentUsername: String = "", // Username собеседника
@ColumnInfo(name = "last_message")
val lastMessage: String = "", // 🔒 Последнее сообщение (зашифрованное для превью)
@ColumnInfo(name = "last_message_timestamp")
val lastMessageTimestamp: Long = 0, // Timestamp последнего сообщения
@ColumnInfo(name = "unread_count") val unreadCount: Int = 0, // Количество непрочитанных
@ColumnInfo(name = "is_online") val isOnline: Int = 0, // Онлайн статус
@ColumnInfo(name = "last_seen") val lastSeen: Long = 0, // Последний раз онлайн
@ColumnInfo(name = "verified") val verified: Int = 0, // Верифицирован
@ColumnInfo(name = "i_have_sent", defaultValue = "0")
val iHaveSent: Int = 0, // Отправлял ли я сообщения в этот диалог (0/1)
@ColumnInfo(name = "last_message_from_me", defaultValue = "0")
val lastMessageFromMe: Int = 0, // Последнее сообщение от меня (0/1)
@ColumnInfo(name = "last_message_delivered", defaultValue = "0")
val lastMessageDelivered: Int =
0, // Статус доставки последнего сообщения (0=WAITING, 1=DELIVERED, 2=ERROR)
@ColumnInfo(name = "last_message_read", defaultValue = "0")
val lastMessageRead: Int = 0, // Прочитано последнее сообщение (0/1)
@ColumnInfo(name = "last_message_attachments", defaultValue = "[]")
val lastMessageAttachments: String =
"[]" // 📎 JSON attachments последнего сообщения (кэш из messages)
)
/** DAO для работы с сообщениями */
@Dao
interface MessageDao {
/** Вставка нового сообщения (IGNORE если уже существует) */
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertMessage(message: MessageEntity): Long
/** Вставка нескольких сообщений (IGNORE если уже существуют) */
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertMessages(messages: List<MessageEntity>)
/** Получить сообщения диалога (постранично) */
@Query(
"""
SELECT * FROM messages
WHERE account = :account AND dialog_key = :dialogKey
ORDER BY timestamp DESC, message_id DESC
LIMIT :limit OFFSET :offset
"""
)
suspend fun getMessages(
account: String,
dialogKey: String,
limit: Int,
offset: Int
): List<MessageEntity>
/**
* 📁 Получить сообщения для Saved Messages (постранично) Специальный метод для случая когда
* from_public_key = to_public_key = account Использует упрощенный запрос без дублирования OR
* условий
*/
@Query(
"""
SELECT * FROM messages
WHERE account = :account
AND from_public_key = :account
AND to_public_key = :account
ORDER BY timestamp DESC, message_id DESC
LIMIT :limit OFFSET :offset
"""
)
suspend fun getMessagesForSavedDialog(
account: String,
limit: Int,
offset: Int
): List<MessageEntity>
/** 📁 Получить количество сообщений в Saved Messages */
@Query(
"""
SELECT COUNT(*) FROM messages
WHERE account = :account
AND from_public_key = :account
AND to_public_key = :account
"""
)
suspend fun getMessageCountForSavedDialog(account: String): Int
/** Получить сообщения диалога как Flow */
@Query(
"""
SELECT * FROM messages
WHERE account = :account AND dialog_key = :dialogKey
ORDER BY timestamp ASC, message_id ASC
"""
)
fun getMessagesFlow(account: String, dialogKey: String): Flow<List<MessageEntity>>
/** Получить количество сообщений в диалоге */
@Query(
"""
SELECT COUNT(*) FROM messages
WHERE account = :account AND dialog_key = :dialogKey
"""
)
suspend fun getMessageCount(account: String, dialogKey: String): Int
/**
* 📸 Получить количество сообщений между двумя пользователями (для проверки первого сообщения
* при отправке аватара)
*/
@Query(
"""
SELECT COUNT(*) FROM messages
WHERE account = :account
AND ((from_public_key = :sender AND to_public_key = :recipient)
OR (from_public_key = :recipient AND to_public_key = :sender))
"""
)
suspend fun getMessageCount(account: String, sender: String, recipient: String): Int
/** Получить последние N сообщений диалога */
@Query(
"""
SELECT * FROM messages
WHERE account = :account AND dialog_key = :dialogKey
ORDER BY timestamp DESC, message_id DESC
LIMIT :limit
"""
)
suspend fun getRecentMessages(
account: String,
dialogKey: String,
limit: Int
): List<MessageEntity>
/** Найти сообщение по ID */
@Query("SELECT * FROM messages WHERE account = :account AND message_id = :messageId LIMIT 1")
suspend fun getMessageById(account: String, messageId: String): MessageEntity?
/** Обновить статус доставки */
@Query(
"UPDATE messages SET delivered = :status WHERE account = :account AND message_id = :messageId"
)
suspend fun updateDeliveryStatus(account: String, messageId: String, status: Int)
/** 🔄 Обновить статус доставки и attachments (для очистки localUri после отправки) */
@Query(
"UPDATE messages SET delivered = :status, attachments = :attachments WHERE account = :account AND message_id = :messageId"
)
suspend fun updateDeliveryStatusAndAttachments(
account: String,
messageId: String,
status: Int,
attachments: String
)
/** Обновить статус прочтения */
@Query("UPDATE messages SET read = 1 WHERE account = :account AND message_id = :messageId")
suspend fun markAsRead(account: String, messageId: String)
/** Отметить все сообщения диалога как прочитанные */
@Query(
"""
UPDATE messages SET read = 1
WHERE account = :account AND dialog_key = :dialogKey AND from_me = 0
"""
)
suspend fun markDialogAsRead(account: String, dialogKey: String)
/** Удалить сообщение */
@Query("DELETE FROM messages WHERE account = :account AND message_id = :messageId")
suspend fun deleteMessage(account: String, messageId: String)
/**
* Найти сообщение по публичному ключу отправителя и timestamp (для reply) Ищет с допуском по
* времени для учета возможных рассинхронизаций
*/
@Query(
"""
SELECT * FROM messages
WHERE account = :account
AND dialog_key = :dialogKey
AND from_public_key = :fromPublicKey
AND timestamp BETWEEN :timestampFrom AND :timestampTo
ORDER BY timestamp ASC, message_id ASC
LIMIT 1
"""
)
suspend fun findMessageByContent(
account: String,
dialogKey: String,
fromPublicKey: String,
timestampFrom: Long,
timestampTo: Long
): MessageEntity?
/**
* Получить количество непрочитанных сообщений для диалога Считает только входящие сообщения
* (from_me = 0) которые не прочитаны (read = 0)
*/
@Query(
"""
SELECT COUNT(*) FROM messages
WHERE account = :account
AND from_public_key = :opponentKey
AND from_me = 0
AND read = 0
"""
)
suspend fun getUnreadCountForDialog(account: String, opponentKey: String): Int
/** Удалить все сообщения диалога (возвращает количество удалённых) */
@Query("DELETE FROM messages WHERE account = :account AND dialog_key = :dialogKey")
suspend fun deleteDialog(account: String, dialogKey: String): Int
/** Удалить все сообщения между двумя пользователями (возвращает количество удалённых) */
@Query(
"""
DELETE FROM messages
WHERE account = :account AND (
(from_public_key = :user1 AND to_public_key = :user2) OR
(from_public_key = :user2 AND to_public_key = :user1)
)
"""
)
suspend fun deleteMessagesBetweenUsers(account: String, user1: String, user2: String): Int
/** Удалить все сообщения аккаунта */
@Query("DELETE FROM messages WHERE account = :account")
suspend fun deleteAllByAccount(account: String): Int
/** Количество непрочитанных сообщений в диалоге */
@Query(
"""
SELECT COUNT(*) FROM messages
WHERE account = :account AND dialog_key = :dialogKey AND from_me = 0 AND read = 0
"""
)
suspend fun getUnreadCount(account: String, dialogKey: String): Int
/** Проверить существование сообщения */
@Query(
"SELECT EXISTS(SELECT 1 FROM messages WHERE account = :account AND message_id = :messageId)"
)
suspend fun messageExists(account: String, messageId: String): Boolean
/**
* Отметить все исходящие сообщения к собеседнику как прочитанные Используется когда приходит
* PacketRead от собеседника 🔥 ВАЖНО: delivered=3 означает READ (синхронизировано с
* ChatViewModel)
*/
@Query(
"""
UPDATE messages SET delivered = 3, read = 1
WHERE account = :account AND to_public_key = :opponent AND from_me = 1
"""
)
suspend fun markAllAsRead(account: String, opponent: String)
/** 🔥 DEBUG: Получить последнее сообщение в диалоге для отладки */
@Query(
"""
SELECT * FROM messages
WHERE account = :account
AND ((from_public_key = :opponent AND to_public_key = :account)
OR (from_public_key = :account AND to_public_key = :opponent))
ORDER BY timestamp DESC, message_id DESC LIMIT 1
"""
)
suspend fun getLastMessageDebug(account: String, opponent: String): MessageEntity?
/**
* 🔥 Получить статус последнего сообщения (fromMe, delivered, read) для диалога Возвращает null
* если сообщений нет
*/
@Query(
"""
SELECT from_me, delivered, read FROM messages
WHERE account = :account
AND ((from_public_key = :opponent AND to_public_key = :account)
OR (from_public_key = :account AND to_public_key = :opponent))
ORDER BY timestamp DESC, message_id DESC LIMIT 1
"""
)
suspend fun getLastMessageStatus(account: String, opponent: String): LastMessageStatus?
/**
* 📎 Получить attachments последнего сообщения для диалога Возвращает null если сообщений нет
* или нет attachments
*/
@Query(
"""
SELECT attachments FROM messages
WHERE account = :account
AND ((from_public_key = :opponent AND to_public_key = :account)
OR (from_public_key = :account AND to_public_key = :opponent))
ORDER BY timestamp DESC, message_id DESC LIMIT 1
"""
)
suspend fun getLastMessageAttachments(account: String, opponent: String): String?
/**
* Get all outgoing messages stuck in WAITING status (delivered = 0).
* Used to retry sending on reconnect (desktop parity: _packetQueue flush).
* Only returns messages younger than minTimestamp to avoid retrying stale messages.
*/
@Query(
"""
SELECT * FROM messages
WHERE account = :account
AND from_me = 1
AND delivered = 0
AND timestamp >= :minTimestamp
ORDER BY timestamp ASC
"""
)
suspend fun getWaitingMessages(account: String, minTimestamp: Long): List<MessageEntity>
/**
* Mark old WAITING messages as ERROR (delivery timeout expired).
* Desktop parity: MESSAGE_MAX_TIME_TO_DELEVERED_S = 80s.
*/
@Query(
"""
UPDATE messages SET delivered = 2
WHERE account = :account
AND from_me = 1
AND delivered = 0
AND timestamp < :maxTimestamp
"""
)
suspend fun markExpiredWaitingAsError(account: String, maxTimestamp: Long): Int
/**
* Update delivery status AND timestamp on delivery confirmation.
* Desktop parity: useDialogFiber.ts sets timestamp = Date.now() on PacketDelivery.
*/
@Query(
"""
UPDATE messages SET delivered = :status, timestamp = :timestamp
WHERE account = :account AND message_id = :messageId
"""
)
suspend fun updateDeliveryStatusAndTimestamp(account: String, messageId: String, status: Int, timestamp: Long)
}
/** DAO для работы с диалогами */
@Dao
interface DialogDao {
/** Вставка/обновление диалога */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDialog(dialog: DialogEntity): Long
/**
* Получить все диалоги отсортированные по последнему сообщению Исключает requests (диалоги без
* исходящих сообщений от нас) Исключает пустые диалоги (без сообщений)
*/
@Query(
"""
SELECT * FROM dialogs
WHERE account = :account
AND i_have_sent = 1
AND last_message_timestamp > 0
ORDER BY last_message_timestamp DESC
LIMIT 30
"""
)
fun getDialogsFlow(account: String): Flow<List<DialogEntity>>
/** Получить все диалоги с пагинацией */
@Query(
"""
SELECT * FROM dialogs
WHERE account = :account
AND i_have_sent = 1
AND last_message_timestamp > 0
ORDER BY last_message_timestamp DESC
LIMIT :limit OFFSET :offset
"""
)
suspend fun getDialogsPaged(account: String, limit: Int, offset: Int): List<DialogEntity>
/**
* Получить requests - диалоги где нам писали, но мы не отвечали Исключает пустые диалоги (без
* сообщений)
*/
@Query(
"""
SELECT * FROM dialogs
WHERE account = :account
AND i_have_sent = 0
AND last_message_timestamp > 0
ORDER BY last_message_timestamp DESC
LIMIT 30
"""
)
fun getRequestsFlow(account: String): Flow<List<DialogEntity>>
/** Получить количество requests Исключает пустые диалоги (без сообщений) */
@Query(
"""
SELECT COUNT(*) FROM dialogs
WHERE account = :account
AND i_have_sent = 0
AND last_message_timestamp > 0
"""
)
fun getRequestsCountFlow(account: String): Flow<Int>
/**
* Desktop parity: get all dialogs where opponent_title is empty or equals the raw
* public key (or its prefix). Used by requestMissingUserInfo() to batch-resolve names
* after sync, like Desktop's useUserInformation per-component hook.
*/
@Query("""
SELECT * FROM dialogs
WHERE account = :account
AND last_message_timestamp > 0
AND (
opponent_title = ''
OR opponent_title = opponent_key
OR LENGTH(opponent_title) <= 8
)
""")
suspend fun getDialogsWithEmptyTitle(account: String): List<DialogEntity>
/** Получить диалог */
@Query("SELECT * FROM dialogs WHERE account = :account AND opponent_key = :opponentKey LIMIT 1")
suspend fun getDialog(account: String, opponentKey: String): DialogEntity?
/** Обновить последнее сообщение */
@Query(
"""
UPDATE dialogs SET
last_message = :lastMessage,
last_message_timestamp = :timestamp
WHERE account = :account AND opponent_key = :opponentKey
"""
)
suspend fun updateLastMessage(
account: String,
opponentKey: String,
lastMessage: String,
timestamp: Long
)
/** Обновить количество непрочитанных */
@Query(
"UPDATE dialogs SET unread_count = :count WHERE account = :account AND opponent_key = :opponentKey"
)
suspend fun updateUnreadCount(account: String, opponentKey: String, count: Int)
/** Инкрементировать непрочитанные */
@Query(
"UPDATE dialogs SET unread_count = unread_count + 1 WHERE account = :account AND opponent_key = :opponentKey"
)
suspend fun incrementUnreadCount(account: String, opponentKey: String)
/** Сбросить непрочитанные */
@Query(
"UPDATE dialogs SET unread_count = 0 WHERE account = :account AND opponent_key = :opponentKey"
)
suspend fun clearUnreadCount(account: String, opponentKey: String)
/** Отметить что я отправлял сообщения в этот диалог Возвращает количество обновлённых строк */
@Query(
"UPDATE dialogs SET i_have_sent = 1 WHERE account = :account AND opponent_key = :opponentKey"
)
suspend fun markIHaveSent(account: String, opponentKey: String): Int
/** Обновить онлайн статус */
@Query(
"""
UPDATE dialogs SET
is_online = :isOnline,
last_seen = :lastSeen
WHERE account = :account AND opponent_key = :opponentKey
"""
)
suspend fun updateOnlineStatus(
account: String,
opponentKey: String,
isOnline: Int,
lastSeen: Long
)
/** Получить онлайн статус пользователя */
@Query(
"""
SELECT is_online, last_seen
FROM dialogs
WHERE account = :account AND opponent_key = :opponentKey
LIMIT 1
"""
)
fun observeOnlineStatus(account: String, opponentKey: String): Flow<OnlineStatusInfo?>
data class OnlineStatusInfo(
@ColumnInfo(name = "is_online") val isOnline: Int,
@ColumnInfo(name = "last_seen") val lastSeen: Long
)
/** Удалить диалог */
@Query("DELETE FROM dialogs WHERE account = :account AND opponent_key = :opponentKey")
suspend fun deleteDialog(account: String, opponentKey: String)
/** Удалить все диалоги аккаунта */
@Query("DELETE FROM dialogs WHERE account = :account")
suspend fun deleteAllByAccount(account: String)
/** Обновить информацию о собеседнике */
@Query(
"""
UPDATE dialogs SET
opponent_title = :title,
opponent_username = :username,
verified = :verified
WHERE account = :account AND opponent_key = :opponentKey
"""
)
suspend fun updateOpponentInfo(
account: String,
opponentKey: String,
title: String,
username: String,
verified: Int
)
/** Обновить только имя/username собеседника, не трогая verified */
@Query(
"""
UPDATE dialogs SET
opponent_title = :title,
opponent_username = :username
WHERE account = :account AND opponent_key = :opponentKey
"""
)
suspend fun updateOpponentDisplayName(
account: String,
opponentKey: String,
title: String,
username: String
)
/**
* Получить общее количество непрочитанных сообщений, исключая указанный диалог Используется для
* отображения badge на кнопке "назад" в экране чата
*/
@Query(
"""
SELECT COALESCE(SUM(unread_count), 0) FROM dialogs
WHERE account = :account AND opponent_key != :excludeOpponentKey
"""
)
fun getTotalUnreadCountExcludingFlow(account: String, excludeOpponentKey: String): Flow<Int>
/** 🚀 Получить последнее сообщение диалога по dialog_key (использует индекс) */
@Query(
"""
SELECT * FROM messages
WHERE account = :account AND dialog_key = :dialogKey
ORDER BY timestamp DESC, message_id DESC LIMIT 1
"""
)
suspend fun getLastMessageByDialogKey(account: String, dialogKey: String): MessageEntity?
/** 🚀 Количество непрочитанных входящих сообщений от оппонента */
@Query(
"""
SELECT COUNT(*) FROM messages
WHERE account = :account
AND from_public_key = :opponentKey
AND to_public_key = :account
AND from_me = 0
AND read = 0
"""
)
suspend fun countUnreadFromOpponent(account: String, opponentKey: String): Int
/** 🚀 Проверить есть ли исходящие сообщения к оппоненту */
@Query(
"""
SELECT EXISTS(
SELECT 1 FROM messages
WHERE account = :account
AND from_public_key = :account
AND to_public_key = :opponentKey
AND from_me = 1
LIMIT 1
)
"""
)
suspend fun hasSentToOpponent(account: String, opponentKey: String): Boolean
/**
* 🚀 ОПТИМИЗИРОВАННЫЙ updateDialogFromMessages Заменяет монолитный SQL с 9 коррелированными
* подзапросами на:
* - 3 индексированных запроса (getLastMessageByDialogKey, countUnreadFromOpponent,
* hasSentToOpponent)
* - 1 SELECT существующего диалога для сохранения метаданных
* - 1 INSERT OR REPLACE Всё обёрнуто в транзакцию для консистентности. Использует dialog_key с
* индексом (account, dialog_key, timestamp) вместо OR-условий.
*/
@Transaction
suspend fun updateDialogFromMessages(account: String, opponentKey: String) {
// 📁 Для saved messages dialogKey = account (не "$account:$account")
val dialogKey =
if (account == opponentKey) account
else if (account < opponentKey) "$account:$opponentKey"
else "$opponentKey:$account"
// 1. Получаем последнее сообщение — O(1) по индексу (account, dialog_key, timestamp)
val lastMsg = getLastMessageByDialogKey(account, dialogKey) ?: return
// 2. Получаем существующий диалог для сохранения метаданных (online, verified, title...)
val existing = getDialog(account, opponentKey)
// 3. Считаем непрочитанные — O(N) по индексу (account, from_public_key, to_public_key,
// timestamp)
val unread = countUnreadFromOpponent(account, opponentKey)
// 4. Проверяем были ли исходящие — O(1)
val hasSent = hasSentToOpponent(account, opponentKey)
// 5. Один INSERT OR REPLACE с вычисленными данными
insertDialog(
DialogEntity(
id = existing?.id ?: 0,
account = account,
opponentKey = opponentKey,
opponentTitle = existing?.opponentTitle ?: "",
opponentUsername = existing?.opponentUsername ?: "",
lastMessage = lastMsg.plainMessage,
lastMessageTimestamp = lastMsg.timestamp,
unreadCount = unread,
isOnline = existing?.isOnline ?: 0,
lastSeen = existing?.lastSeen ?: 0,
verified = existing?.verified ?: 0,
iHaveSent = if (hasSent) 1 else (existing?.iHaveSent ?: 0),
lastMessageFromMe = lastMsg.fromMe,
lastMessageDelivered = if (lastMsg.fromMe == 1) lastMsg.delivered else 0,
lastMessageRead = if (lastMsg.fromMe == 1) lastMsg.read else 0,
lastMessageAttachments = lastMsg.attachments
)
)
}
/**
* 📁 ОПТИМИЗИРОВАННЫЙ updateSavedMessagesDialogFromMessages Для saved messages (opponentKey ==
* account)
*/
@Transaction
suspend fun updateSavedMessagesDialogFromMessages(account: String) {
// 📁 dialogKey для saved messages = account (не "$account:$account")
val dialogKey = account
val lastMsg = getLastMessageByDialogKey(account, dialogKey) ?: return
val existing = getDialog(account, account)
insertDialog(
DialogEntity(
id = existing?.id ?: 0,
account = account,
opponentKey = account,
opponentTitle = existing?.opponentTitle ?: "",
opponentUsername = existing?.opponentUsername ?: "",
lastMessage = lastMsg.plainMessage,
lastMessageTimestamp = lastMsg.timestamp,
unreadCount = 0,
isOnline = existing?.isOnline ?: 0,
lastSeen = existing?.lastSeen ?: 0,
verified = existing?.verified ?: 0,
iHaveSent = 1,
lastMessageFromMe = 1,
lastMessageDelivered = 1,
lastMessageRead = 1,
lastMessageAttachments = lastMsg.attachments
)
)
}
}