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() { 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 ): 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() ) }