mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-11-29 01:48:53 +03:00
remove SyncableModel and SyncUtils
not needed for our requirements + it simplifies code Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
parent
a84e69b6bb
commit
fe4bf942ab
9 changed files with 46 additions and 175 deletions
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud Talk - Android Client
|
* Nextcloud Talk - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||||
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
@ -161,7 +162,7 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||||
|
|
||||||
val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId)
|
val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId)
|
||||||
|
|
||||||
if (loadFromServer && monitor.isOnline.first()) {
|
if (loadFromServer) {
|
||||||
sync(withNetworkParams)
|
sync(withNetworkParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,7 +293,6 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||||
val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId)
|
val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId)
|
||||||
|
|
||||||
if (loadFromServer) {
|
if (loadFromServer) {
|
||||||
|
|
||||||
val fieldMap = getFieldMap(
|
val fieldMap = getFieldMap(
|
||||||
lookIntoFuture = false,
|
lookIntoFuture = false,
|
||||||
includeLastKnown = true,
|
includeLastKnown = true,
|
||||||
|
@ -368,6 +368,11 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sync(bundle: Bundle): List<ChatMessageEntity>? {
|
private suspend fun sync(bundle: Bundle): List<ChatMessageEntity>? {
|
||||||
|
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()
|
val result = getMessagesFromServer(bundle) ?: return listOf()
|
||||||
var chatMessagesFromSync: List<ChatMessageEntity>? = null
|
var chatMessagesFromSync: List<ChatMessageEntity>? = null
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,11 @@
|
||||||
|
|
||||||
package com.nextcloud.talk.conversationlist.data
|
package com.nextcloud.talk.conversationlist.data
|
||||||
|
|
||||||
import com.nextcloud.talk.data.sync.Syncable
|
|
||||||
import com.nextcloud.talk.models.domain.ConversationModel
|
import com.nextcloud.talk.models.domain.ConversationModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface OfflineConversationsRepository : Syncable {
|
interface OfflineConversationsRepository {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream of a list of rooms, for use in the conversation list.
|
* Stream of a list of rooms, for use in the conversation list.
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud Talk - Android Client
|
* Nextcloud Talk - Android Client
|
||||||
*
|
*
|
||||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||||
|
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.nextcloud.talk.conversationlist.data.network
|
package com.nextcloud.talk.conversationlist.data.network
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.util.Log
|
||||||
import androidx.core.os.bundleOf
|
import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository
|
||||||
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
|
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
|
||||||
import com.nextcloud.talk.data.database.dao.ConversationsDao
|
import com.nextcloud.talk.data.database.dao.ConversationsDao
|
||||||
import com.nextcloud.talk.data.database.mappers.asEntity
|
import com.nextcloud.talk.data.database.mappers.asEntity
|
||||||
import com.nextcloud.talk.data.database.mappers.asModel
|
import com.nextcloud.talk.data.database.mappers.asModel
|
||||||
import com.nextcloud.talk.data.database.model.ConversationEntity
|
import com.nextcloud.talk.data.database.model.ConversationEntity
|
||||||
import com.nextcloud.talk.data.sync.Synchronizer
|
import com.nextcloud.talk.data.network.NetworkMonitor
|
||||||
import com.nextcloud.talk.data.sync.changeListSync
|
|
||||||
import com.nextcloud.talk.data.user.model.User
|
import com.nextcloud.talk.data.user.model.User
|
||||||
import com.nextcloud.talk.models.domain.ConversationModel
|
import com.nextcloud.talk.models.domain.ConversationModel
|
||||||
import com.nextcloud.talk.models.json.conversations.Conversation
|
|
||||||
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
|
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
@ -35,9 +34,9 @@ import javax.inject.Inject
|
||||||
class OfflineFirstConversationsRepository @Inject constructor(
|
class OfflineFirstConversationsRepository @Inject constructor(
|
||||||
private val dao: ConversationsDao,
|
private val dao: ConversationsDao,
|
||||||
private val network: ConversationsNetworkDataSource,
|
private val network: ConversationsNetworkDataSource,
|
||||||
|
private val monitor: NetworkMonitor,
|
||||||
private val currentUserProviderNew: CurrentUserProviderNew
|
private val currentUserProviderNew: CurrentUserProviderNew
|
||||||
) : OfflineConversationsRepository, Synchronizer {
|
) : OfflineConversationsRepository {
|
||||||
|
|
||||||
override val roomListFlow: Flow<List<ConversationModel>>
|
override val roomListFlow: Flow<List<ConversationModel>>
|
||||||
get() = _roomListFlow
|
get() = _roomListFlow
|
||||||
private val _roomListFlow: MutableSharedFlow<List<ConversationModel>> = MutableSharedFlow()
|
private val _roomListFlow: MutableSharedFlow<List<ConversationModel>> = MutableSharedFlow()
|
||||||
|
@ -56,7 +55,7 @@ class OfflineFirstConversationsRepository @Inject constructor(
|
||||||
if (list.isNotEmpty()) {
|
if (list.isNotEmpty()) {
|
||||||
_roomListFlow.emit(list)
|
_roomListFlow.emit(list)
|
||||||
}
|
}
|
||||||
this@OfflineFirstConversationsRepository.sync(bundleOf())
|
sync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,39 +63,28 @@ class OfflineFirstConversationsRepository @Inject constructor(
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val id = user.id!!
|
val id = user.id!!
|
||||||
val model = getConversation(id, roomToken)
|
val model = getConversation(id, roomToken)
|
||||||
model?.let { _conversationFlow.emit(model) }
|
model.let { _conversationFlow.emit(model) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun syncWith(bundle: Bundle, synchronizer: Synchronizer): Boolean =
|
private suspend fun sync() {
|
||||||
synchronizer.changeListSync(
|
if (!monitor.isOnline.first()) {
|
||||||
modelFetcher = {
|
Log.d(OfflineFirstChatRepository.TAG, "Device is offline, can't load conversations from server")
|
||||||
return@changeListSync getConversationsFromServer()
|
return
|
||||||
},
|
}
|
||||||
// not needed
|
|
||||||
versionUpdater = {},
|
|
||||||
modelDeleter = {},
|
|
||||||
modelUpdater = { models ->
|
|
||||||
val list = models.filterIsInstance<Conversation>().map {
|
|
||||||
it.asEntity(user.id!!)
|
|
||||||
}
|
|
||||||
dao.upsertConversations(list)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getConversationsFromServer(): List<Conversation> {
|
try {
|
||||||
val list = network.getRooms(user, user.baseUrl!!, false)
|
val conversationsList = network.getRooms(user, user.baseUrl!!, false)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.map { list ->
|
.blockingSingle()
|
||||||
return@map list.map {
|
|
||||||
it.apply {
|
|
||||||
id = roomId!!.toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.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<ConversationModel> =
|
private suspend fun getListOfConversations(accountId: Long): List<ConversationModel> =
|
||||||
|
@ -104,8 +92,12 @@ class OfflineFirstConversationsRepository @Inject constructor(
|
||||||
it.map(ConversationEntity::asModel)
|
it.map(ConversationEntity::asModel)
|
||||||
}.first()
|
}.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()
|
val entity = dao.getConversationForUser(accountId, token).first()
|
||||||
return entity?.asModel()
|
return entity.asModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = OfflineFirstConversationsRepository::class.simpleName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,9 +191,10 @@ class RepositoryModule {
|
||||||
fun provideOfflineFirstConversationsRepository(
|
fun provideOfflineFirstConversationsRepository(
|
||||||
dao: ConversationsDao,
|
dao: ConversationsDao,
|
||||||
dataSource: ConversationsNetworkDataSource,
|
dataSource: ConversationsNetworkDataSource,
|
||||||
|
networkMonitor: NetworkMonitor,
|
||||||
currentUserProviderNew: CurrentUserProviderNew
|
currentUserProviderNew: CurrentUserProviderNew
|
||||||
): OfflineConversationsRepository {
|
): OfflineConversationsRepository {
|
||||||
return OfflineFirstConversationsRepository(dao, dataSource, currentUserProviderNew)
|
return OfflineFirstConversationsRepository(dao, dataSource, networkMonitor, currentUserProviderNew)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* Nextcloud Talk - Android Client
|
|
||||||
*
|
|
||||||
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
|
||||||
* 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
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
/*
|
|
||||||
* Nextcloud Talk - Android Client
|
|
||||||
*
|
|
||||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
|
||||||
* 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 <T> suspendRunCatching(block: suspend () -> T): Result<T> =
|
|
||||||
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<SyncableModel>,
|
|
||||||
versionUpdater: (Long) -> Unit,
|
|
||||||
modelDeleter: suspend (List<Long>) -> Unit,
|
|
||||||
modelUpdater: suspend (List<SyncableModel>) -> 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
|
|
|
@ -7,7 +7,6 @@
|
||||||
*/
|
*/
|
||||||
package com.nextcloud.talk.models.domain
|
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.data.user.model.User
|
||||||
import com.nextcloud.talk.models.json.chat.ChatMessageJson
|
import com.nextcloud.talk.models.json.chat.ChatMessageJson
|
||||||
import com.nextcloud.talk.models.json.conversations.Conversation
|
import com.nextcloud.talk.models.json.conversations.Conversation
|
||||||
|
@ -62,9 +61,7 @@ class ConversationModel(
|
||||||
var recordingConsentRequired: Int = 0,
|
var recordingConsentRequired: Int = 0,
|
||||||
var remoteServer: String? = null,
|
var remoteServer: String? = null,
|
||||||
var remoteToken: String? = null,
|
var remoteToken: String? = null,
|
||||||
override var id: Long = roomId?.toLong() ?: 0,
|
) {
|
||||||
override var markedForDeletion: Boolean = false
|
|
||||||
) : SyncableModel {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel {
|
fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud Talk - Android Client
|
* Nextcloud Talk - Android Client
|
||||||
*
|
*
|
||||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ package com.nextcloud.talk.models.json.chat
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.bluelinelabs.logansquare.annotation.JsonField
|
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||||
import com.bluelinelabs.logansquare.annotation.JsonObject
|
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.chat.data.model.ChatMessage.SystemMessageType
|
||||||
import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
|
import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
|
@ -12,10 +12,9 @@ package com.nextcloud.talk.models.json.conversations
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.bluelinelabs.logansquare.annotation.JsonField
|
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||||
import com.bluelinelabs.logansquare.annotation.JsonObject
|
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.data.user.model.User
|
||||||
import com.nextcloud.talk.models.domain.ConversationModel
|
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.ConversationObjectTypeConverter
|
||||||
import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter
|
import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter
|
||||||
import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter
|
import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter
|
||||||
|
@ -156,10 +155,7 @@ data class Conversation(
|
||||||
@JsonField(name = ["remoteToken"])
|
@JsonField(name = ["remoteToken"])
|
||||||
var remoteToken: String? = null,
|
var remoteToken: String? = null,
|
||||||
|
|
||||||
override var id: Long = 0,
|
) : Parcelable {
|
||||||
override var markedForDeletion: Boolean = false
|
|
||||||
|
|
||||||
) : Parcelable, SyncableModel {
|
|
||||||
@Deprecated("Use ConversationUtil")
|
@Deprecated("Use ConversationUtil")
|
||||||
val isPublic: Boolean
|
val isPublic: Boolean
|
||||||
get() = ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == type
|
get() = ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == type
|
||||||
|
|
Loading…
Reference in a new issue