Вторая часть по кэшу
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user