611 lines
24 KiB
Kotlin
611 lines
24 KiB
Kotlin
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)
|
||
}
|