diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt index 094882c48..7fc9416c9 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -1,6 +1,7 @@ /* * Nextcloud Talk - Android Client * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe * SPDX-FileCopyrightText: 2024 Julius Linus * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -161,7 +162,7 @@ class OfflineFirstChatRepository @Inject constructor( val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId) - if (loadFromServer && monitor.isOnline.first()) { + if (loadFromServer) { sync(withNetworkParams) } @@ -292,7 +293,6 @@ class OfflineFirstChatRepository @Inject constructor( val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId) if (loadFromServer) { - val fieldMap = getFieldMap( lookIntoFuture = false, includeLastKnown = true, @@ -368,6 +368,11 @@ class OfflineFirstChatRepository @Inject constructor( } private suspend fun sync(bundle: Bundle): List? { + if (!monitor.isOnline.first()) { + Log.d(TAG, "Device is offline, can't load chat messages from server") + return null + } + val result = getMessagesFromServer(bundle) ?: return listOf() var chatMessagesFromSync: List? = null diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt index d89023365..a264a84ee 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt @@ -7,12 +7,11 @@ package com.nextcloud.talk.conversationlist.data -import com.nextcloud.talk.data.sync.Syncable import com.nextcloud.talk.models.domain.ConversationModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow -interface OfflineConversationsRepository : Syncable { +interface OfflineConversationsRepository { /** * Stream of a list of rooms, for use in the conversation list. diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt index f3b59d15d..6043c9345 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt @@ -1,24 +1,23 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-FileCopyrightText: 2024 Marcel Hibbe * SPDX-License-Identifier: GPL-3.0-or-later */ package com.nextcloud.talk.conversationlist.data.network -import android.os.Bundle -import androidx.core.os.bundleOf +import android.util.Log +import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.data.database.dao.ConversationsDao import com.nextcloud.talk.data.database.mappers.asEntity import com.nextcloud.talk.data.database.mappers.asModel import com.nextcloud.talk.data.database.model.ConversationEntity -import com.nextcloud.talk.data.sync.Synchronizer -import com.nextcloud.talk.data.sync.changeListSync +import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel -import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -35,9 +34,9 @@ import javax.inject.Inject class OfflineFirstConversationsRepository @Inject constructor( private val dao: ConversationsDao, private val network: ConversationsNetworkDataSource, + private val monitor: NetworkMonitor, private val currentUserProviderNew: CurrentUserProviderNew -) : OfflineConversationsRepository, Synchronizer { - +) : OfflineConversationsRepository { override val roomListFlow: Flow> get() = _roomListFlow private val _roomListFlow: MutableSharedFlow> = MutableSharedFlow() @@ -56,7 +55,7 @@ class OfflineFirstConversationsRepository @Inject constructor( if (list.isNotEmpty()) { _roomListFlow.emit(list) } - this@OfflineFirstConversationsRepository.sync(bundleOf()) + sync() } } @@ -64,39 +63,28 @@ class OfflineFirstConversationsRepository @Inject constructor( scope.launch { val id = user.id!! val model = getConversation(id, roomToken) - model?.let { _conversationFlow.emit(model) } + model.let { _conversationFlow.emit(model) } } - override suspend fun syncWith(bundle: Bundle, synchronizer: Synchronizer): Boolean = - synchronizer.changeListSync( - modelFetcher = { - return@changeListSync getConversationsFromServer() - }, - // not needed - versionUpdater = {}, - modelDeleter = {}, - modelUpdater = { models -> - val list = models.filterIsInstance().map { - it.asEntity(user.id!!) - } - dao.upsertConversations(list) - } - ) + private suspend fun sync() { + if (!monitor.isOnline.first()) { + Log.d(OfflineFirstChatRepository.TAG, "Device is offline, can't load conversations from server") + return + } - private fun getConversationsFromServer(): List { - val list = network.getRooms(user, user.baseUrl!!, false) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .map { list -> - return@map list.map { - it.apply { - id = roomId!!.toLong() - } - } - } - .blockingSingle() + try { + val conversationsList = network.getRooms(user, user.baseUrl!!, false) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .blockingSingle() - return list ?: listOf() + val list = conversationsList.map { + it.asEntity(user.id!!) + } + dao.upsertConversations(list) + } catch (e: Exception) { + Log.e(TAG, "Something went wrong when fetching conversations", e) + } } private suspend fun getListOfConversations(accountId: Long): List = @@ -104,8 +92,12 @@ class OfflineFirstConversationsRepository @Inject constructor( it.map(ConversationEntity::asModel) }.first() - private suspend fun getConversation(accountId: Long, token: String): ConversationModel? { + private suspend fun getConversation(accountId: Long, token: String): ConversationModel { val entity = dao.getConversationForUser(accountId, token).first() - return entity?.asModel() + return entity.asModel() + } + + companion object { + val TAG = OfflineFirstConversationsRepository::class.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 3159f7e6b..666ab69d0 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -191,9 +191,10 @@ class RepositoryModule { fun provideOfflineFirstConversationsRepository( dao: ConversationsDao, dataSource: ConversationsNetworkDataSource, + networkMonitor: NetworkMonitor, currentUserProviderNew: CurrentUserProviderNew ): OfflineConversationsRepository { - return OfflineFirstConversationsRepository(dao, dataSource, currentUserProviderNew) + return OfflineFirstConversationsRepository(dao, dataSource, networkMonitor, currentUserProviderNew) } @Provides diff --git a/app/src/main/java/com/nextcloud/talk/data/changeListVersion/SyncableModel.kt b/app/src/main/java/com/nextcloud/talk/data/changeListVersion/SyncableModel.kt deleted file mode 100644 index 6173d9e9b..000000000 --- a/app/src/main/java/com/nextcloud/talk/data/changeListVersion/SyncableModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2024 Julius Linus - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.nextcloud.talk.data.changeListVersion - -/** - * Models any changes from the network, agnostic to what data is being modeled. - * Implemented by Models that support offline synchronization. - */ -interface SyncableModel { - - /** - * Model identifier. - */ - var id: Long - - /** - * Model deletion checker. - */ - var markedForDeletion: Boolean -} diff --git a/app/src/main/java/com/nextcloud/talk/data/sync/SyncUtils.kt b/app/src/main/java/com/nextcloud/talk/data/sync/SyncUtils.kt deleted file mode 100644 index 3218a9d32..000000000 --- a/app/src/main/java/com/nextcloud/talk/data/sync/SyncUtils.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2024 Your Name - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.nextcloud.talk.data.sync - -import android.os.Bundle -import android.util.Log -import com.nextcloud.talk.data.changeListVersion.SyncableModel -import kotlin.coroutines.cancellation.CancellationException - -/** - * Interface marker for a class that manages synchronization between local data and a remote - * source for a [Syncable]. - */ -interface Synchronizer { - - // TODO include any other helper functions here that the Synchronizer needs - - /** - * Syntactic sugar to call [Syncable.syncWith] while omitting the synchronizer argument - */ - suspend fun Syncable.sync(bundle: Bundle) = this@sync.syncWith(bundle, this@Synchronizer) -} - -/** - * Interface marker for a class that is synchronized with a remote source. Syncing must not be - * performed concurrently and it is the [Synchronizer]'s responsibility to ensure this. - */ -interface Syncable { - /** - * Synchronizes the local database backing the repository with the network. - * Takes in a [bundle] to retrieve other metadata needed - * - * Returns if the sync was successful or not. - */ - suspend fun syncWith(bundle: Bundle, synchronizer: Synchronizer): Boolean -} - -/** - * Attempts [block], returning a successful [Result] if it succeeds, otherwise a [Result.Failure] - * taking care not to break structured concurrency - */ -private suspend fun suspendRunCatching(block: suspend () -> T): Result = - try { - Result.success(block()) - } catch (cancellationException: CancellationException) { - throw cancellationException - } catch (exception: Exception) { - Log.e( - "suspendRunCatching", - "Failed to evaluate a suspendRunCatchingBlock. Returning failure Result", - exception - ) - Result.failure(exception) - } - -/** - * Utility function for syncing a repository with the network. - * [modelFetcher] Fetches the change list for the model - * [versionUpdater] Updates the version after a successful sync - * [modelDeleter] Deletes models by consuming the ids of the models that have been deleted. - * [modelUpdater] Updates models by consuming the ids of the models that have changed. - * - * Note that the blocks defined above are never run concurrently, and the [Synchronizer] - * implementation must guarantee this. - */ -suspend fun Synchronizer.changeListSync( - modelFetcher: suspend () -> List, - versionUpdater: (Long) -> Unit, - modelDeleter: suspend (List) -> Unit, - modelUpdater: suspend (List) -> Unit -) = suspendRunCatching { - // Fetch the change list since last sync (akin to a git fetch) - val changeList = modelFetcher() - if (changeList.isEmpty()) return@suspendRunCatching true - - // Splits the models marked for deletion from the ones that are updated or new - val (deleted, updated) = changeList.partition(SyncableModel::markedForDeletion) - - // Delete models that have been deleted server-side - modelDeleter(deleted.map(SyncableModel::id)) - - // Using the fetch list, pull down and upsert the changes (akin to a git pull) - modelUpdater(updated) - - // Update the last synced version (akin to updating local git HEAD) - val latestVersion = changeList.last().id - versionUpdater(latestVersion) -}.isSuccess diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt index c752f78e8..3fef24bdc 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt @@ -7,7 +7,6 @@ */ package com.nextcloud.talk.models.domain -import com.nextcloud.talk.data.changeListVersion.SyncableModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.chat.ChatMessageJson import com.nextcloud.talk.models.json.conversations.Conversation @@ -62,9 +61,7 @@ class ConversationModel( var recordingConsentRequired: Int = 0, var remoteServer: String? = null, var remoteToken: String? = null, - override var id: Long = roomId?.toLong() ?: 0, - override var markedForDeletion: Boolean = false -) : SyncableModel { +) { companion object { fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt index 01a984c66..90068d945 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt @@ -1,7 +1,7 @@ /* * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2024 Marcel Hibbe * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -10,7 +10,6 @@ package com.nextcloud.talk.models.json.chat import android.os.Parcelable import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonObject -import com.nextcloud.talk.data.changeListVersion.SyncableModel import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter import kotlinx.parcelize.Parcelize diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index 82e1dd087..17b563aa4 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -12,10 +12,9 @@ package com.nextcloud.talk.models.json.conversations import android.os.Parcelable import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonObject -import com.nextcloud.talk.models.json.chat.ChatMessageJson -import com.nextcloud.talk.data.changeListVersion.SyncableModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.chat.ChatMessageJson import com.nextcloud.talk.models.json.converters.ConversationObjectTypeConverter import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter @@ -156,10 +155,7 @@ data class Conversation( @JsonField(name = ["remoteToken"]) var remoteToken: String? = null, - override var id: Long = 0, - override var markedForDeletion: Boolean = false - -) : Parcelable, SyncableModel { +) : Parcelable { @Deprecated("Use ConversationUtil") val isPublic: Boolean get() = ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == type