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

611 lines
24 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
/**
* 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)
)
/**
* 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
/**
* Получить последние 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)
/**
* Обновить статус прочтения
*/
@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
/**
* Отметить все исходящие сообщения к собеседнику как прочитанные (delivered=3)
* Используется когда приходит PacketRead от собеседника
*/
@Query("""
UPDATE messages SET delivered = 3
WHERE account = :account AND to_public_key = :opponent AND from_me = 1 AND delivered < 3
""")
suspend fun markAllAsRead(account: String, opponent: 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("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>
/**
* Обновить диалог, пересчитав счетчики из таблицы messages
* Этот метод аналогичен updateDialog из Архива - обновляет все поля диалога одним запросом
*
* Логика:
* 1. Берем последнее сообщение (по timestamp DESC)
* 2. Считаем количество непрочитанных сообщений (from_me = 0 AND read = 0)
* 3. Вычисляем i_have_sent = 1 если есть исходящие сообщения (from_me = 1) - как sended в Архиве
* 4. Обновляем диалог или создаем новый ТОЛЬКО если есть сообщения!
*/
@Query("""
INSERT OR REPLACE INTO dialogs (
account,
opponent_key,
opponent_title,
opponent_username,
last_message,
last_message_timestamp,
unread_count,
is_online,
last_seen,
verified,
i_have_sent
)
SELECT
:account AS account,
:opponentKey AS opponent_key,
COALESCE(
(SELECT opponent_title FROM dialogs WHERE account = :account AND opponent_key = :opponentKey),
''
) AS opponent_title,
COALESCE(
(SELECT opponent_username FROM dialogs WHERE account = :account AND opponent_key = :opponentKey),
''
) AS opponent_username,
COALESCE(
(SELECT plain_message FROM messages
WHERE account = :account
AND ((from_public_key = :opponentKey AND to_public_key = :account)
OR (from_public_key = :account AND to_public_key = :opponentKey))
ORDER BY timestamp DESC LIMIT 1),
''
) AS last_message,
COALESCE(
(SELECT MAX(timestamp) FROM messages
WHERE account = :account
AND ((from_public_key = :opponentKey AND to_public_key = :account)
OR (from_public_key = :account AND to_public_key = :opponentKey))),
0
) AS last_message_timestamp,
COALESCE(
(SELECT COUNT(*) FROM messages
WHERE account = :account
AND from_public_key = :opponentKey
AND to_public_key = :account
AND from_me = 0
AND read = 0),
0
) AS unread_count,
COALESCE(
(SELECT is_online FROM dialogs WHERE account = :account AND opponent_key = :opponentKey),
0
) AS is_online,
COALESCE(
(SELECT last_seen FROM dialogs WHERE account = :account AND opponent_key = :opponentKey),
0
) AS last_seen,
COALESCE(
(SELECT verified FROM dialogs WHERE account = :account AND opponent_key = :opponentKey),
0
) AS verified,
CASE
WHEN (SELECT COUNT(*) FROM messages
WHERE account = :account
AND from_public_key = :account
AND to_public_key = :opponentKey
AND from_me = 1) > 0
THEN 1
ELSE COALESCE(
(SELECT i_have_sent FROM dialogs WHERE account = :account AND opponent_key = :opponentKey),
0
)
END AS i_have_sent
WHERE EXISTS (
SELECT 1 FROM messages
WHERE account = :account
AND ((from_public_key = :opponentKey AND to_public_key = :account)
OR (from_public_key = :account AND to_public_key = :opponentKey))
)
""")
suspend fun updateDialogFromMessages(account: String, opponentKey: String)
/**
* 📁 Обновить Saved Messages диалог, пересчитав счетчики из таблицы messages
* Специальный метод для случая когда opponentKey == account (saved messages)
* Использует упрощенный запрос без дублирования OR условий
*
* Ключевые отличия от обычного updateDialogFromMessages:
* 1. Упрощенные WHERE условия: from_public_key = :account AND to_public_key = :account
* 2. unread_count всегда 0 (нельзя иметь непрочитанные от самого себя)
* 3. i_have_sent всегда 1 (все сообщения исходящие)
*/
@Query("""
INSERT OR REPLACE INTO dialogs (
account,
opponent_key,
opponent_title,
opponent_username,
last_message,
last_message_timestamp,
unread_count,
is_online,
last_seen,
verified,
i_have_sent
)
SELECT
:account AS account,
:account AS opponent_key,
COALESCE(
(SELECT opponent_title FROM dialogs WHERE account = :account AND opponent_key = :account),
''
) AS opponent_title,
COALESCE(
(SELECT opponent_username FROM dialogs WHERE account = :account AND opponent_key = :account),
''
) AS opponent_username,
COALESCE(
(SELECT plain_message FROM messages
WHERE account = :account
AND from_public_key = :account
AND to_public_key = :account
ORDER BY timestamp DESC LIMIT 1),
''
) AS last_message,
COALESCE(
(SELECT MAX(timestamp) FROM messages
WHERE account = :account
AND from_public_key = :account
AND to_public_key = :account),
0
) AS last_message_timestamp,
0 AS unread_count,
COALESCE(
(SELECT is_online FROM dialogs WHERE account = :account AND opponent_key = :account),
0
) AS is_online,
COALESCE(
(SELECT last_seen FROM dialogs WHERE account = :account AND opponent_key = :account),
0
) AS last_seen,
COALESCE(
(SELECT verified FROM dialogs WHERE account = :account AND opponent_key = :account),
0
) AS verified,
1 AS i_have_sent
WHERE EXISTS (
SELECT 1 FROM messages
WHERE account = :account
AND from_public_key = :account
AND to_public_key = :account
)
""")
suspend fun updateSavedMessagesDialogFromMessages(account: String)
}