Кэш чатов
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
package core.database.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Статус синхронизации сообщения с сервером
|
||||
*/
|
||||
enum class SyncStatus {
|
||||
SYNCED, // Сообщение успешно синхронизировано
|
||||
SYNCING, // Сообщение отправляется на сервер
|
||||
FAILED // Ошибка синхронизации
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity для хранения сообщений в локальной базе данных
|
||||
* Поддерживает офлайн-работу и фоновую синхронизацию
|
||||
*/
|
||||
@Entity(tableName = "messages")
|
||||
data class MessageEntity(
|
||||
@PrimaryKey val id: String,
|
||||
@@ -14,20 +28,102 @@ data class MessageEntity(
|
||||
val sequenceId: Int,
|
||||
val createdAt: String,
|
||||
val mediaType: String,
|
||||
val mediaJson: String, // Simplified for now
|
||||
val mediaJson: String,
|
||||
val reactionsJson: String,
|
||||
val isRead: Boolean,
|
||||
val replyToId: String? = null
|
||||
val replyToId: String? = null,
|
||||
|
||||
// Поля для офлайн-синхронизации
|
||||
@ColumnInfo(defaultValue = "SYNCED")
|
||||
val syncStatus: SyncStatus = SyncStatus.SYNCED,
|
||||
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
val isDeletedLocally: Boolean = false,
|
||||
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
val isEditedLocally: Boolean = false,
|
||||
|
||||
val editedContent: String? = null,
|
||||
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
val lastUpdated: Long = 0
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface MessageDao {
|
||||
@Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId ASC")
|
||||
// ==================== Flow для UI ====================
|
||||
|
||||
@Query("SELECT * FROM messages WHERE chatId = :chatId AND isDeletedLocally = 0 ORDER BY sequenceId ASC")
|
||||
fun getMessages(chatId: String): Flow<List<MessageEntity>>
|
||||
|
||||
// ==================== Paging 3 ====================
|
||||
|
||||
@Query("SELECT * FROM messages WHERE chatId = :chatId AND isDeletedLocally = 0 ORDER BY sequenceId DESC")
|
||||
fun getMessagesPagingSource(chatId: String): PagingSource<Int, MessageEntity>
|
||||
|
||||
// Загружаем сообщения С МЕНЬШИМ sequenceId (старые), порядок DESC для PagingSource
|
||||
@Query("SELECT * FROM messages WHERE chatId = :chatId AND isDeletedLocally = 0 AND sequenceId < :sequenceId ORDER BY sequenceId DESC LIMIT :limit")
|
||||
suspend fun getMessagesBefore(chatId: String, sequenceId: Int, limit: Int): List<MessageEntity>
|
||||
|
||||
// Загружаем сообщения до и ВКЛЮЧАЯ sequenceId, порядок DESC
|
||||
@Query("SELECT * FROM messages WHERE chatId = :chatId AND isDeletedLocally = 0 AND sequenceId <= :sequenceId ORDER BY sequenceId DESC LIMIT :limit")
|
||||
suspend fun getMessagesUpToAndIncluding(chatId: String, sequenceId: Int, limit: Int): List<MessageEntity>
|
||||
|
||||
// Загружаем сообщения С БОЛЬШИМ sequenceId (новые), порядок ASC
|
||||
@Query("SELECT * FROM messages WHERE chatId = :chatId AND isDeletedLocally = 0 AND sequenceId > :sequenceId ORDER BY sequenceId ASC LIMIT :limit")
|
||||
suspend fun getMessagesAfter(chatId: String, sequenceId: Int, limit: Int): List<MessageEntity>
|
||||
|
||||
@Query("SELECT MAX(sequenceId) FROM messages WHERE chatId = :chatId AND isDeletedLocally = 0")
|
||||
suspend fun getMaxSequenceId(chatId: String): Int?
|
||||
|
||||
@Query("SELECT MIN(sequenceId) FROM messages WHERE chatId = :chatId AND isDeletedLocally = 0")
|
||||
suspend fun getMinSequenceId(chatId: String): Int?
|
||||
|
||||
// ==================== Основные операции ====================
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertMessages(messages: List<MessageEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertMessage(message: MessageEntity)
|
||||
|
||||
/**
|
||||
* Upsert - вставляет или обновляет сообщение
|
||||
* Приоритет: серверные данные > локальные (кроме сообщений в процессе отправки)
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun upsertMessage(message: MessageEntity) {
|
||||
val existing = getMessageById(message.id)
|
||||
if (existing != null) {
|
||||
// Сохраняем локальные изменения если сообщение в процессе отправки
|
||||
val syncedMessage = when {
|
||||
existing.syncStatus == SyncStatus.SYNCING || existing.isEditedLocally -> {
|
||||
message.copy(
|
||||
syncStatus = existing.syncStatus,
|
||||
isEditedLocally = existing.isEditedLocally,
|
||||
editedContent = existing.editedContent,
|
||||
lastUpdated = existing.lastUpdated
|
||||
)
|
||||
}
|
||||
else -> message
|
||||
}
|
||||
insertMessage(syncedMessage)
|
||||
} else {
|
||||
insertMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
suspend fun upsertMessages(messages: List<MessageEntity>) {
|
||||
messages.forEach { upsertMessage(it) }
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM messages WHERE id = :id LIMIT 1")
|
||||
suspend fun getMessageById(id: String): MessageEntity?
|
||||
|
||||
@Query("SELECT * FROM messages WHERE id = :id LIMIT 1")
|
||||
fun getMessageByIdFlow(id: String): Flow<MessageEntity?>
|
||||
|
||||
@Query("DELETE FROM messages WHERE chatId = :chatId")
|
||||
suspend fun clearChat(chatId: String)
|
||||
|
||||
@@ -36,9 +132,78 @@ interface MessageDao {
|
||||
|
||||
@Query("UPDATE messages SET isRead = 1 WHERE chatId = :chatId AND sequenceId <= :lastReadSequenceId AND isRead = 0")
|
||||
suspend fun markMessagesAsRead(chatId: String, lastReadSequenceId: Int)
|
||||
|
||||
// ==================== Офлайн операции ====================
|
||||
|
||||
/**
|
||||
* Помечает сообщение как удалённое локально
|
||||
* Фактическое удаление произойдёт после синхронизации с сервером
|
||||
*/
|
||||
@Query("UPDATE messages SET isDeletedLocally = 1, syncStatus = 'SYNCING', lastUpdated = :timestamp WHERE id = :messageId")
|
||||
suspend fun markAsDeletedLocally(messageId: String, timestamp: Long = System.currentTimeMillis())
|
||||
|
||||
/**
|
||||
* Помечает сообщение как отредактированное локально
|
||||
*/
|
||||
@Query("UPDATE messages SET isEditedLocally = 1, editedContent = :newContent, syncStatus = 'SYNCING', lastUpdated = :timestamp WHERE id = :messageId")
|
||||
suspend fun markAsEditedLocally(messageId: String, newContent: String, timestamp: Long = System.currentTimeMillis())
|
||||
|
||||
/**
|
||||
* Сбрасывает локальные изменения после успешной синхронизации
|
||||
*/
|
||||
@Query("UPDATE messages SET syncStatus = 'SYNCED', isDeletedLocally = 0, isEditedLocally = 0, editedContent = NULL WHERE id = :messageId")
|
||||
suspend fun markAsSynced(messageId: String)
|
||||
|
||||
/**
|
||||
* Устанавливает статус FAILED для сообщений с ошибкой синхронизации
|
||||
*/
|
||||
@Query("UPDATE messages SET syncStatus = 'FAILED' WHERE id = :messageId")
|
||||
suspend fun markAsSyncFailed(messageId: String)
|
||||
|
||||
/**
|
||||
* Получает все сообщения, требующие синхронизации
|
||||
*/
|
||||
@Query("SELECT * FROM messages WHERE syncStatus = 'SYNCING' OR isDeletedLocally = 1 ORDER BY lastUpdated ASC")
|
||||
suspend fun getPendingSyncMessages(): List<MessageEntity>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE syncStatus = 'SYNCING' OR isDeletedLocally = 1 ORDER BY lastUpdated ASC")
|
||||
fun getPendingSyncMessagesFlow(): Flow<List<MessageEntity>>
|
||||
|
||||
/**
|
||||
* Получает сообщения с_failed статусом для повторной отправки
|
||||
*/
|
||||
@Query("SELECT * FROM messages WHERE syncStatus = 'FAILED' ORDER BY lastUpdated ASC")
|
||||
suspend fun getFailedSyncMessages(): List<MessageEntity>
|
||||
|
||||
// ==================== Утилиты ====================
|
||||
|
||||
@Query("SELECT COUNT(*) FROM messages WHERE chatId = :chatId AND isDeletedLocally = 0")
|
||||
suspend fun getMessagesCount(chatId: String): Int
|
||||
|
||||
@Query("SELECT COUNT(*) FROM messages WHERE chatId = :chatId AND isRead = 0 AND isDeletedLocally = 0")
|
||||
suspend fun getUnreadCount(chatId: String): Int
|
||||
|
||||
@Query("SELECT EXISTS(SELECT 1 FROM messages WHERE id = :id)")
|
||||
suspend fun exists(id: String): Boolean
|
||||
}
|
||||
|
||||
@Database(entities = [MessageEntity::class], version = 1)
|
||||
@Database(entities = [MessageEntity::class], version = 2)
|
||||
@TypeConverters(SyncStatusConverter::class)
|
||||
abstract class ChatDatabase : RoomDatabase() {
|
||||
abstract fun messageDao(): MessageDao
|
||||
|
||||
companion object {
|
||||
const val DATABASE_NAME = "knot_chat_database"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертер для Enum SyncStatus
|
||||
*/
|
||||
class SyncStatusConverter {
|
||||
@TypeConverter
|
||||
fun fromSyncStatus(status: SyncStatus): String = status.name
|
||||
|
||||
@TypeConverter
|
||||
fun toSyncStatus(value: String): SyncStatus = SyncStatus.valueOf(value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user