204 lines
8.1 KiB
Kotlin
204 lines
8.1 KiB
Kotlin
package chats.data.paging
|
|
|
|
import android.util.Log
|
|
import androidx.paging.*
|
|
import chats.data.remote.api.ChatApi
|
|
import chats.data.remote.dto.MessageDto
|
|
import chats.domain.model.MediaType
|
|
import com.google.gson.Gson
|
|
import core.database.data.ChatDatabase
|
|
import core.database.data.MessageDao
|
|
import core.database.data.MessageEntity
|
|
import core.database.data.SyncStatus
|
|
import core.network.ServerConfig
|
|
import core.security.TokenManager
|
|
|
|
/**
|
|
* RemoteMediator для Paging 3
|
|
* Управляет загрузкой сообщений из сети и кэшированием в Room
|
|
*
|
|
* Логика работы:
|
|
* 1. При первой загрузке (REFRESH) - загружаем последние сообщения
|
|
* 2. При прокрутке вниз (APPEND) - загружаем более старые сообщения
|
|
* 3. При прокрутке вверх (PREPEND) - загружаем более новые сообщения
|
|
* 4. Данные сохраняются в Room, Paging читает из базы
|
|
*/
|
|
@OptIn(ExperimentalPagingApi::class)
|
|
class MessageRemoteMediator(
|
|
private val chatId: String,
|
|
private val api: ChatApi,
|
|
private val database: ChatDatabase,
|
|
private val dao: core.database.data.MessageDao,
|
|
private val serverConfig: ServerConfig,
|
|
private val tokenManager: TokenManager
|
|
) : RemoteMediator<Int, MessageEntity>() {
|
|
|
|
private val gson = Gson()
|
|
private val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
|
|
|
/**
|
|
* Состояние пагинации
|
|
*/
|
|
data class MessageRemoteMediatorState(
|
|
val lastSequenceId: Int?,
|
|
val firstSequenceId: Int?
|
|
)
|
|
|
|
override suspend fun initialize(): InitializeAction {
|
|
// Всегда запускаем refresh для проверки кэша
|
|
return InitializeAction.LAUNCH_INITIAL_REFRESH
|
|
}
|
|
|
|
override suspend fun load(
|
|
loadType: LoadType,
|
|
state: PagingState<Int, MessageEntity>
|
|
): MediatorResult {
|
|
Log.d(TAG, ">>> load() loadType=$loadType")
|
|
return try {
|
|
// Проверяем наличие данных в локальной БД
|
|
val localMinSeq = dao.getMinSequenceId(chatId)
|
|
val localMaxSeq = dao.getMaxSequenceId(chatId)
|
|
Log.d(TAG, "Local DB: minSeq=$localMinSeq, maxSeq=$localMaxSeq")
|
|
|
|
when (loadType) {
|
|
LoadType.REFRESH -> {
|
|
// Если есть локальные данные - не делаем API запрос
|
|
if (localMaxSeq != null) {
|
|
Log.d(TAG, "REFRESH: Using cached data (maxSeq=$localMaxSeq)")
|
|
return MediatorResult.Success(endOfPaginationReached = false)
|
|
}
|
|
// Нет данных - загружаем последние сообщения
|
|
Log.d(TAG, "REFRESH: No cached data, fetching from API")
|
|
}
|
|
LoadType.APPEND -> {
|
|
Log.d(TAG, "APPEND: Loading older messages")
|
|
}
|
|
LoadType.PREPEND -> {
|
|
Log.d(TAG, "PREPEND: Loading newer messages")
|
|
}
|
|
}
|
|
|
|
// Определяем pivot для API запроса
|
|
val pivotValue: Long? = when (loadType) {
|
|
LoadType.REFRESH -> null // Последние сообщения
|
|
LoadType.APPEND -> {
|
|
// Старые сообщения - берём минимальный sequenceId из текущей страницы
|
|
(state.pages.lastOrNull()?.data?.lastOrNull()?.sequenceId
|
|
?: localMinSeq)?.toLong()?.minus(1)
|
|
}
|
|
LoadType.PREPEND -> {
|
|
// Новые сообщения - берём максимальный sequenceId
|
|
(state.pages.firstOrNull()?.data?.firstOrNull()?.sequenceId
|
|
?: localMaxSeq)?.toLong()?.plus(1)
|
|
}
|
|
}
|
|
|
|
val limit = when (loadType) {
|
|
LoadType.REFRESH -> state.config.initialLoadSize
|
|
else -> state.config.pageSize
|
|
}
|
|
|
|
Log.d(TAG, "API call: chatId=$chatId, pivot=$pivotValue, limit=$limit")
|
|
|
|
// Выполняем запрос к API
|
|
val messages = try {
|
|
api.getMessages(
|
|
chatId = chatId,
|
|
cursor = null,
|
|
pivot = pivotValue,
|
|
limit = limit
|
|
)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "API call failed - offline?", e)
|
|
// При ошибке сети и наличии локальных данных - успех
|
|
if ((localMinSeq != null || localMaxSeq != null)) {
|
|
Log.d(TAG, "Offline mode: returning success with cached data")
|
|
return MediatorResult.Success(endOfPaginationReached = true)
|
|
}
|
|
throw e
|
|
}
|
|
|
|
Log.d(TAG, "Received ${messages.size} messages from API")
|
|
|
|
if (messages.isEmpty()) {
|
|
Log.d(TAG, "End of pagination - no more messages")
|
|
return MediatorResult.Success(endOfPaginationReached = true)
|
|
}
|
|
|
|
// Сохраняем в базу
|
|
val currentUserId = tokenManager.getUserId() ?: ""
|
|
val entities = messages.map { dto ->
|
|
dto.toEntity(currentUserId, baseUrl, gson)
|
|
}
|
|
Log.d(TAG, "Saving ${entities.size} messages to DB, first seq=${entities.firstOrNull()?.sequenceId}, last seq=${entities.lastOrNull()?.sequenceId}")
|
|
dao.upsertMessages(entities)
|
|
|
|
// Проверяем что данные сохранились
|
|
val savedCount = dao.getMessagesCount(chatId)
|
|
Log.d(TAG, "Total messages in DB after save: $savedCount")
|
|
|
|
val endOfPaginationReached = messages.size < limit
|
|
Log.d(TAG, "endOfPaginationReached=$endOfPaginationReached")
|
|
MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
|
|
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Error loading messages", e)
|
|
MediatorResult.Error(e)
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "MessageRemoteMediator"
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extension function для преобразования DTO в Entity
|
|
*/
|
|
private fun MessageDto.toEntity(
|
|
currentUserId: String,
|
|
baseUrl: String,
|
|
gson: Gson
|
|
): MessageEntity {
|
|
// Определяем тип медиа
|
|
val mediaType = when {
|
|
media.isEmpty() -> MediaType.TEXT.name
|
|
media.any { it.type.startsWith("image") || it.url.endsWith(".gif") } -> MediaType.GIF.name
|
|
media.any { it.type.startsWith("image") } -> MediaType.IMAGE.name
|
|
media.any { it.type.startsWith("video") } -> MediaType.VIDEO.name
|
|
media.any { it.type.startsWith("audio") } -> MediaType.AUDIO.name
|
|
else -> MediaType.FILE.name
|
|
}
|
|
|
|
// Сериализуем медиа в JSON
|
|
val mediaJson = gson.toJson(media)
|
|
|
|
// Сериализуем реакции в JSON
|
|
val reactionsMap = reactions?.associate { it.emoji to it.count } ?: emptyMap()
|
|
val reactionsJson = gson.toJson(reactionsMap)
|
|
|
|
// Определяем, прочитано ли сообщение текущим пользователем
|
|
val isRead = senderId == currentUserId
|
|
|
|
return MessageEntity(
|
|
id = id,
|
|
chatId = chatId ?: "",
|
|
senderId = senderId ?: "",
|
|
senderName = sender?.displayName ?: sender?.username ?: "Unknown",
|
|
senderAvatar = sender?.avatarUrl,
|
|
content = content,
|
|
sequenceId = sequenceId ?: 0,
|
|
createdAt = createdAt ?: "",
|
|
mediaType = mediaType,
|
|
mediaJson = mediaJson,
|
|
reactionsJson = reactionsJson,
|
|
isRead = isRead,
|
|
replyToId = replyTo?.id,
|
|
syncStatus = SyncStatus.SYNCED,
|
|
isDeletedLocally = false,
|
|
isEditedLocally = false,
|
|
editedContent = null,
|
|
lastUpdated = System.currentTimeMillis()
|
|
)
|
|
}
|