Files
mobile-android/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt
k1ngsterr1 11a8ff7644 Refactor SwipeBackContainer for improved performance and readability
- Added lazy composition to skip setup until the screen is first opened, reducing allocations.
- Cleaned up code formatting for better readability.
- Enhanced comments for clarity on functionality.
- Streamlined gesture handling logic for swipe detection and animation.
2026-02-08 07:34:25 +05:00

631 lines
26 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
/** 🔥 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
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
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
"""
)
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
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
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(
"""
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, 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, 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, id DESC LIMIT 1
"""
)
suspend fun getLastMessageAttachments(account: String, opponent: String): String?
}
/** 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
"""
)
fun getDialogsFlow(account: String): Flow<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
"""
)
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>
/** Получить диалог */
@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(
"""
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
)
/**
* Получить общее количество непрочитанных сообщений, исключая указанный диалог Используется для
* отображения 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, 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) {
val dialogKey =
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) {
val dialogKey = "$account:$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
)
)
}
}