Вторая часть по кэшу

This commit is contained in:
Халимов Рустам
2026-05-08 22:48:56 +03:00
parent d4c41b333a
commit a0b50b57b0
21 changed files with 237 additions and 377 deletions
@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface ChatDao {
@Query("SELECT * FROM chats ORDER BY updatedAtMillis DESC")
@Query("SELECT * FROM chats ORDER BY lastMessageTimestamp DESC")
fun getAllChats(): Flow<List<ChatEntity>>
@Query("SELECT * FROM chats WHERE remoteId = :remoteId LIMIT 1")
@@ -29,7 +29,7 @@ interface ChatDao {
suspend fun updateChat(chat: ChatEntity)
@Query("UPDATE chats SET lastMessageText = :lastMessageText, lastMessageTimestamp = :timestamp WHERE remoteId = :chatId")
suspend fun updateLastMessage(chatId: String, lastMessageText: String?, timestamp: String?)
suspend fun updateLastMessage(chatId: String, lastMessageText: String?, timestamp: Long?)
@Query("UPDATE chats SET unreadCount = :count WHERE remoteId = :chatId")
suspend fun updateUnreadCount(chatId: String, count: Int)
@@ -14,6 +14,19 @@ interface MessageDao {
@Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId ASC")
fun getMessagesByChatId(chatId: String): Flow<List<MessageEntity>>
/**
* Пагинированная загрузка сообщений для Paging 3
* sequenceId увеличивается от старых к новым, поэтому ORDER BY DESC для отображения newest внизу
*/
@Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId DESC LIMIT :limit OFFSET :offset")
suspend fun getMessagesPaged(chatId: String, offset: Int, limit: Int): List<MessageEntity>
@Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId DESC LIMIT 1")
suspend fun getLastMessage(chatId: String): MessageEntity?
@Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sequenceId ASC LIMIT 1")
suspend fun getFirstMessage(chatId: String): MessageEntity?
@Query("SELECT * FROM messages WHERE chatId = :chatId AND sequenceId > :afterSequenceId ORDER BY sequenceId ASC LIMIT :limit")
suspend fun getMessagesAfter(chatId: String, afterSequenceId: Long, limit: Int): List<MessageEntity>
@@ -31,7 +31,7 @@ data class ChatEntity(
val lastMessageText: String? = null,
val lastMessageTimestamp: String? = null,
val lastMessageTimestamp: Long? = null,
val updatedAtMillis: Long = System.currentTimeMillis()
)
@@ -7,6 +7,7 @@ import chats.domain.model.Chat
* Преобразует Domain Chat в Entity
*/
fun Chat.toEntity(): ChatEntity {
val timestamp = this.lastMessage?.createdAt?.let { parseTimestamp(it) }
return ChatEntity(
localId = this.id.takeIf { it.isNotBlank() } ?: java.util.UUID.randomUUID().toString(),
remoteId = this.id,
@@ -15,7 +16,7 @@ fun Chat.toEntity(): ChatEntity {
avatar = this.avatar,
unreadCount = this.unreadCount,
lastMessageText = this.lastMessage?.content,
lastMessageTimestamp = this.lastMessage?.createdAt
lastMessageTimestamp = timestamp
)
}
@@ -32,3 +33,14 @@ fun ChatEntity.toDomain(): Chat {
lastMessage = null // lastMessage загружается отдельно
)
}
/**
* Парсит ISO-8601 timestamp в Long (millis)
*/
private fun parseTimestamp(isoTimestamp: String): Long? {
return try {
java.time.ZonedDateTime.parse(isoTimestamp).toInstant().toEpochMilli()
} catch (e: Exception) {
null
}
}
@@ -0,0 +1,43 @@
package chats.data.local.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import chats.data.local.dao.MessageDao
import chats.data.local.database.MessageEntity
/**
* PagingSource для загрузки сообщений из Room Database
*/
class MessagePagingSource(
private val chatId: String,
private val messageDao: MessageDao
) : PagingSource<Int, MessageEntity>() {
companion object {
private const val PAGE_SIZE = 30
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MessageEntity> {
return try {
val position = params.key ?: 0 // Начинаем с 0
val messages = messageDao.getMessagesPaged(
chatId = chatId,
offset = position,
limit = PAGE_SIZE
)
LoadResult.Page(
data = messages,
prevKey = if (position > 0) position - PAGE_SIZE else null,
nextKey = if (messages.isEmpty()) null else position + PAGE_SIZE
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, MessageEntity>): Int? {
return state.anchorPosition
}
}
@@ -0,0 +1,101 @@
package chats.data.local.paging
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import chats.data.local.dao.ChatDao
import chats.data.local.dao.MessageDao
import chats.data.local.database.AppDatabase
import chats.data.local.database.MessageEntity
import chats.data.local.mappers.toEntity
import chats.data.repository.toDomain as dtoToDomain
import chats.data.remote.api.ChatApi
import chats.domain.model.MessageStatus
import core.security.TokenManager
/**
* RemoteMediator для синхронизации сообщений с сервером.
* Работает с Room через withTransaction для атомарности.
*/
@OptIn(ExperimentalPagingApi::class)
class MessageRemoteMediator(
private val chatId: String,
private val messageDao: MessageDao,
private val chatDao: ChatDao,
private val chatApi: ChatApi,
private val tokenManager: TokenManager,
private val appDatabase: AppDatabase
) : RemoteMediator<Int, MessageEntity>() {
companion object {
private const val PAGE_SIZE = 30
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, MessageEntity>
): RemoteMediator.MediatorResult {
return try {
val currentUserId = tokenManager.getUserId() ?: ""
val baseUrl = "https://your-server.com"
val cursor = when (loadType) {
LoadType.REFRESH -> {
// При refresh загружаем новые сообщения после последнего локального
messageDao.getLastMessage(chatId)?.serverId
}
LoadType.PREPEND -> {
// Загружаем более старые сообщения перед первым локальным
messageDao.getFirstMessage(chatId)?.serverId
}
LoadType.APPEND -> {
// Загружаем новые сообщения после последнего локального
messageDao.getLastMessage(chatId)?.serverId
}
}
val remoteMessages = chatApi.getMessages(chatId, cursor = cursor, limit = PAGE_SIZE)
val endOfPaginationReached = remoteMessages.size < PAGE_SIZE
appDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
// При полном обновлении можно очистить старые SENT/DELIVERED/READ,
// но оставить локальные PENDING/FAILED
// messageDao.deleteMessagesByChatId(chatId) // Опционально
}
val entities = remoteMessages.map { dto ->
val existing = messageDao.getMessageByServerId(dto.id)
if (existing != null) {
// Обновляем существующее, сохраняя localId
existing.copy(
content = dto.content,
reactionsJson = com.google.gson.Gson().toJson(
dto.reactions?.associate { it.emoji to it.count } ?: emptyMap<String, Int>()
),
status = if (dto.senderId == currentUserId) MessageStatus.SENT else MessageStatus.DELIVERED,
updatedAtMillis = System.currentTimeMillis()
)
} else {
// Создаём новое
dto.dtoToDomain(currentUserId, baseUrl).toEntity(MessageStatus.SENT).copy(
localId = java.util.UUID.randomUUID().toString(),
serverId = dto.id,
idempotencyKey = dto.id
)
}
}
messageDao.insertMessages(entities)
}
RemoteMediator.MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: Exception) {
RemoteMediator.MediatorResult.Error(e)
}
}
override suspend fun initialize(): InitializeAction = InitializeAction.SKIP_INITIAL_REFRESH
}