diff --git a/.github/workflows/generate-dokka.yml b/.github/workflows/generate-dokka.yml index 30c7414c965..01e2e6e5aaa 100644 --- a/.github/workflows/generate-dokka.yml +++ b/.github/workflows/generate-dokka.yml @@ -25,7 +25,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy docs 🚀 - uses: JamesIves/github-pages-deploy-action@v4.2.5 + uses: JamesIves/github-pages-deploy-action@v4.6.9 with: branch: gh-pages clean: false diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt index 293cf4e6fcb..a74f1903193 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt @@ -293,6 +293,7 @@ sealed class ConversationDetails(open val conversation: Conversation) { override val conversation: Conversation, val otherUser: OtherUser, val userType: UserType, + val isFavorite: Boolean = false ) : ConversationDetails(conversation) data class Group( @@ -300,7 +301,8 @@ sealed class ConversationDetails(open val conversation: Conversation) { val hasOngoingCall: Boolean = false, val isSelfUserMember: Boolean, val isSelfUserCreator: Boolean, - val selfRole: Conversation.Member.Role? + val selfRole: Conversation.Member.Role?, + val isFavorite: Boolean = false // val isTeamAdmin: Boolean, TODO kubaz ) : ConversationDetails(conversation) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt index 54f10b8df38..faef6688735 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt @@ -31,15 +31,18 @@ import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.logic.wrapStorageRequest -import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi.Companion.MIN_API_VERSION import com.wire.kalium.network.api.base.unbound.versioning.VersionApi +import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO import com.wire.kalium.persistence.daokaliumdb.ServerConfigurationDAO import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import io.ktor.http.Url import kotlinx.coroutines.withContext -internal interface ServerConfigRepository { +interface ServerConfigRepository { + val minimumApiVersionForPersonalToTeamAccountMigration: Int + suspend fun getOrFetchMetadata(serverLinks: ServerConfig.Links): Either suspend fun storeConfig(links: ServerConfig.Links, metadata: ServerConfig.MetaData): Either @@ -62,6 +65,7 @@ internal interface ServerConfigRepository { * Return the server links and metadata for the given userId */ suspend fun configForUser(userId: UserId): Either + suspend fun commonApiVersion(domain: String): Either } @Suppress("LongParameterList", "TooManyFunctions") @@ -72,6 +76,8 @@ internal class ServerConfigDataSource( private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : ServerConfigRepository { + override val minimumApiVersionForPersonalToTeamAccountMigration = MIN_API_VERSION + override suspend fun getOrFetchMetadata(serverLinks: ServerConfig.Links): Either = wrapStorageRequest { dao.configByLinks(serverConfigMapper.toEntity(serverLinks)) }.fold({ fetchApiVersionAndStore(serverLinks) @@ -127,13 +133,17 @@ internal class ServerConfigDataSource( } override suspend fun updateConfigApiVersion(serverConfig: ServerConfig): Either = - fetchMetadata(serverConfig.links) - .flatMap { wrapStorageRequest { dao.updateApiVersion(serverConfig.id, it.commonApiVersion.version) } } + fetchMetadata(serverConfig.links) + .flatMap { wrapStorageRequest { dao.updateApiVersion(serverConfig.id, it.commonApiVersion.version) } } override suspend fun configForUser(userId: UserId): Either = wrapStorageRequest { dao.configForUser(userId.toDao()) } .map { serverConfigMapper.fromEntity(it) } + override suspend fun commonApiVersion(domain: String): Either = wrapStorageRequest { + dao.getCommonApiVersion(domain) + } + private suspend fun fetchMetadata(serverLinks: ServerConfig.Links): Either = wrapApiRequest { versionApi.fetchApiVersion(Url(serverLinks.api)) } .flatMap { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt index 7a37f8ca9e2..2f8ec71e3de 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt @@ -263,6 +263,7 @@ internal class ConversationMapperImpl( activeOneOnOneConversationId = userActiveOneOnOneConversationId?.toModel() ), userType = domainUserTypeMapper.fromUserTypeEntity(userType), + isFavorite = isFavorite ) } @@ -272,7 +273,8 @@ internal class ConversationMapperImpl( hasOngoingCall = callStatus != null, // todo: we can do better! isSelfUserMember = isMember, isSelfUserCreator = isCreator == 1L, - selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) } + selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, + isFavorite = isFavorite ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt index 997efe7ba17..fc20894926b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt @@ -61,6 +61,13 @@ fun ConversationFolderEntity.toModel() = ConversationFolder( type = type.toModel() ) +fun FolderWithConversationsEntity.toModel() = FolderWithConversations( + id = id, + name = name, + type = type.toModel(), + conversationIdList = conversationIdList.map { it.toModel() } +) + fun FolderWithConversations.toDao() = FolderWithConversationsEntity( id = id, name = name, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt index caf5173737c..70ad373bdfa 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.data.conversation.folders import com.benasher44.uuid.uuid4 import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.CONVERSATIONS_FOLDERS +import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents @@ -27,8 +28,10 @@ import com.wire.kalium.logic.data.conversation.ConversationMapper import com.wire.kalium.logic.data.conversation.FolderType import com.wire.kalium.logic.data.conversation.FolderWithConversations import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.flatMapLeft import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.functional.onFailure @@ -51,6 +54,9 @@ internal interface ConversationFolderRepository { suspend fun observeConversationsFromFolder(folderId: String): Flow> suspend fun updateConversationFolders(folderWithConversations: List): Either suspend fun fetchConversationFolders(): Either + suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either + suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either + suspend fun syncConversationFoldersFromLocal(): Either } internal class ConversationFolderDataSource internal constructor( @@ -119,4 +125,31 @@ internal class ConversationFolderDataSource internal constructor( } .map { } + override suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS) + .v("Adding conversation ${conversationId.toLogString()} to folder ${folderId.obfuscateId()}") + return wrapStorageRequest { + conversationFolderDAO.addConversationToFolder(conversationId.toDao(), folderId) + } + } + + override suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS) + .v("Removing conversation ${conversationId.toLogString()} from folder ${folderId.obfuscateId()}") + return wrapStorageRequest { + conversationFolderDAO.removeConversationFromFolder(conversationId.toDao(), folderId) + } + } + + override suspend fun syncConversationFoldersFromLocal(): Either { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Syncing conversation folders from local") + return wrapStorageRequest { conversationFolderDAO.getFoldersWithConversations().map { it.toModel() } } + .flatMap { + wrapApiRequest { + userPropertiesApi.updateLabels( + LabelListResponseDTO(it.map { it.toLabel() }) + ) + } + } + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index d218ed15118..af2eb6eb898 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -1893,6 +1893,8 @@ class UserSessionScope internal constructor( isE2EIEnabled, certificateRevocationListRepository, incrementalSyncRepository, + sessionManager, + selfTeamId, checkRevocationList, syncFeatureConfigsUseCase, userScopedLogger diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index 7ea0b3ac407..0f3be756ff3 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -49,10 +49,14 @@ import com.wire.kalium.logic.feature.connection.MarkConnectionRequestAsNotifiedU import com.wire.kalium.logic.feature.connection.ObserveConnectionListUseCase import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCase import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCaseImpl import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCaseImpl @@ -353,5 +357,8 @@ class ConversationScope internal constructor( get() = ObserveConversationsFromFolderUseCaseImpl(conversationFolderRepository) val getFavoriteFolder: GetFavoriteFolderUseCase get() = GetFavoriteFolderUseCaseImpl(conversationFolderRepository) - + val addConversationToFavorites: AddConversationToFavoritesUseCase + get() = AddConversationToFavoritesUseCaseImpl(conversationFolderRepository) + val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase + get() = RemoveConversationFromFavoritesUseCaseImpl(conversationFolderRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCase.kt new file mode 100644 index 00000000000..994aae91ab7 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCase.kt @@ -0,0 +1,71 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * This use case will add a conversation to the favorites folder. + */ +interface AddConversationToFavoritesUseCase { + /** + * @param conversationId the id of the conversation + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke(conversationId: ConversationId): Result + + sealed interface Result { + data object Success : Result + data class Failure(val cause: CoreFailure) : Result + } +} + +internal class AddConversationToFavoritesUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : AddConversationToFavoritesUseCase { + override suspend fun invoke( + conversationId: ConversationId + ): AddConversationToFavoritesUseCase.Result = withContext(dispatchers.io) { + conversationFolderRepository.getFavoriteConversationFolder().fold( + { AddConversationToFavoritesUseCase.Result.Failure(it) }, + { folder -> + conversationFolderRepository.addConversationToFolder( + conversationId, + folder.id + ) + .flatMap { + conversationFolderRepository.syncConversationFoldersFromLocal() + } + .fold({ + AddConversationToFavoritesUseCase.Result.Failure(it) + }, { + AddConversationToFavoritesUseCase.Result.Success + }) + } + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCase.kt new file mode 100644 index 00000000000..6c927230411 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCase.kt @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * This use case will remove a conversation from the favorites folder. + */ +interface RemoveConversationFromFavoritesUseCase { + /** + * @param conversationId the id of the conversation + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke(conversationId: ConversationId): Result + + sealed interface Result { + data object Success : Result + data class Failure(val cause: CoreFailure) : Result + } +} + +internal class RemoveConversationFromFavoritesUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : RemoveConversationFromFavoritesUseCase { + override suspend fun invoke( + conversationId: ConversationId + ): RemoveConversationFromFavoritesUseCase.Result = withContext(dispatchers.io) { + conversationFolderRepository.getFavoriteConversationFolder().fold( + { RemoveConversationFromFavoritesUseCase.Result.Failure(it) }, + { folder -> + conversationFolderRepository.removeConversationFromFolder(conversationId, folder.id) + .flatMap { + conversationFolderRepository.syncConversationFoldersFromLocal() + } + .fold({ + RemoveConversationFromFavoritesUseCase.Result.Failure(it) + }, { + RemoveConversationFromFavoritesUseCase.Result.Success + }) + } + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCase.kt new file mode 100644 index 00000000000..be386f4e509 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCase.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +@file:Suppress("konsist.useCasesShouldNotAccessNetworkLayerDirectly") + +package com.wire.kalium.logic.feature.personaltoteamaccount + +import com.wire.kalium.logic.configuration.server.ServerConfigRepository +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.network.session.SessionManager + +/** + * Use case to check if the user can migrate from personal to team account. + * The user can migrate if the user is not in a team and the server supports the migration. + */ +interface CanMigrateFromPersonalToTeamUseCase { + suspend operator fun invoke(): Boolean +} + +internal class CanMigrateFromPersonalToTeamUseCaseImpl( + val sessionManager: SessionManager, + val serverConfigRepository: ServerConfigRepository, + val selfTeamIdProvider: SelfTeamIdProvider +) : CanMigrateFromPersonalToTeamUseCase { + override suspend fun invoke(): Boolean { + val commonApiVersion = sessionManager.serverConfig().metaData.commonApiVersion.version + val minApi = serverConfigRepository.minimumApiVersionForPersonalToTeamAccountMigration + return selfTeamIdProvider().fold( + { false }, + { teamId -> teamId == null && commonApiVersion >= minApi } + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt index 82c9a0bd336..9bae507ae54 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -@file:Suppress("konsist.useCasesShouldNotAccessDaoLayerDirectly") +@file:Suppress("konsist.useCasesShouldNotAccessDaoLayerDirectly", "konsist.useCasesShouldNotAccessNetworkLayerDirectly") package com.wire.kalium.logic.feature.user @@ -31,6 +31,7 @@ import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository import com.wire.kalium.logic.data.e2ei.E2EIRepository import com.wire.kalium.logic.data.e2ei.RevocationListChecker import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.id.SelfTeamIdProvider import com.wire.kalium.logic.data.properties.UserPropertyRepository import com.wire.kalium.logic.data.session.SessionRepository import com.wire.kalium.logic.data.sync.IncrementalSyncRepository @@ -67,6 +68,8 @@ import com.wire.kalium.logic.feature.featureConfig.FeatureFlagSyncWorkerImpl import com.wire.kalium.logic.feature.featureConfig.FeatureFlagsSyncWorker import com.wire.kalium.logic.feature.featureConfig.SyncFeatureConfigsUseCase import com.wire.kalium.logic.feature.message.MessageSender +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCaseImpl import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCaseImpl import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase @@ -81,6 +84,7 @@ import com.wire.kalium.logic.feature.user.typingIndicator.ObserveTypingIndicator import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCase import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCaseImpl import com.wire.kalium.logic.sync.SyncManager +import com.wire.kalium.network.session.SessionManager import com.wire.kalium.persistence.dao.MetadataDAO @Suppress("LongParameterList") @@ -109,6 +113,8 @@ class UserScope internal constructor( private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase, private val certificateRevocationListRepository: CertificateRevocationListRepository, private val incrementalSyncRepository: IncrementalSyncRepository, + private val sessionManager: SessionManager, + private val selfTeamIdProvider: SelfTeamIdProvider, private val checkRevocationList: RevocationListChecker, private val syncFeatureConfigs: SyncFeatureConfigsUseCase, private val userScopedLogger: KaliumLogger @@ -208,13 +214,14 @@ class UserScope internal constructor( kaliumLogger = userScopedLogger, ) - val syncCertificateRevocationListUseCase: SyncCertificateRevocationListUseCase get() = - SyncCertificateRevocationListUseCase( - certificateRevocationListRepository = certificateRevocationListRepository, - incrementalSyncRepository = incrementalSyncRepository, - revocationListChecker = checkRevocationList, - kaliumLogger = userScopedLogger, - ) + val syncCertificateRevocationListUseCase: SyncCertificateRevocationListUseCase + get() = + SyncCertificateRevocationListUseCase( + certificateRevocationListRepository = certificateRevocationListRepository, + incrementalSyncRepository = incrementalSyncRepository, + revocationListChecker = checkRevocationList, + kaliumLogger = userScopedLogger, + ) val featureFlagsSyncWorker: FeatureFlagsSyncWorker by lazy { FeatureFlagSyncWorkerImpl( @@ -223,4 +230,11 @@ class UserScope internal constructor( kaliumLogger = userScopedLogger, ) } + val isPersonalToTeamAccountSupportedByBackend: CanMigrateFromPersonalToTeamUseCase by lazy { + CanMigrateFromPersonalToTeamUseCaseImpl( + sessionManager = sessionManager, + serverConfigRepository = serverConfigRepository, + selfTeamIdProvider = selfTeamIdProvider + ) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt index fb655297cb8..7249c4da12f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext +// TODO: Move this class to logic module @OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList") class SessionManagerImpl internal constructor( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt index 722ad9e3d81..a7bce64bfa5 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt @@ -210,7 +210,7 @@ internal class SlowSyncManager( * Useful when a new step is added to Slow Sync, or when we fix some bug in Slow Sync, * and we'd like to get all users to take advantage of the fix. */ - const val CURRENT_VERSION = 8 + const val CURRENT_VERSION = 9 val MIN_RETRY_DELAY = 1.seconds val MAX_RETRY_DELAY = 10.minutes diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt index 8b830cb30dc..77407d39b67 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.conversation.folders import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.conversation.FolderType import com.wire.kalium.logic.data.conversation.FolderWithConversations +import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestUser @@ -155,6 +156,66 @@ class ConversationFolderRepositoryTest { coVerify { arrangement.conversationFolderDAO.updateConversationFolders(any()) }.wasInvoked() } + @Test + fun givenValidConversationAndFolderWhenAddingConversationThenShouldAddSuccessfully() = runTest { + // given + val folderId = "folder1" + val conversationId = TestConversation.ID + val arrangement = Arrangement() + .withAddConversationToFolder() + .withGetFoldersWithConversations() + .withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200)) + + // when + val result = arrangement.repository.addConversationToFolder(conversationId, folderId) + + // then + result.shouldSucceed() + coVerify { arrangement.conversationFolderDAO.addConversationToFolder(eq(conversationId.toDao()), eq(folderId)) }.wasInvoked() + } + + @Test + fun givenValidConversationAndFolderWhenRemovingConversationThenShouldRemoveSuccessfully() = runTest { + // given + val folderId = "folder1" + val conversationId = TestConversation.ID + val arrangement = Arrangement() + .withRemoveConversationFromFolder() + .withGetFoldersWithConversations() + .withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200)) + + // when + val result = arrangement.repository.removeConversationFromFolder(conversationId, folderId) + + // then + result.shouldSucceed() + coVerify { arrangement.conversationFolderDAO.removeConversationFromFolder(eq(conversationId.toDao()), eq(folderId)) }.wasInvoked() + } + + @Test + fun givenLocalFoldersWhenSyncingFoldersThenShouldUpdateSuccessfully() = runTest { + // given + val folders = listOf( + FolderWithConversations( + id = "folder1", + name = "Favorites", + type = FolderType.FAVORITE, + conversationIdList = emptyList() + ) + ) + val arrangement = Arrangement() + .withGetFoldersWithConversations(folders) + .withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200)) + + // when + val result = arrangement.repository.syncConversationFoldersFromLocal() + + // then + result.shouldSucceed() + coVerify { arrangement.userPropertiesApi.updateLabels(any()) }.wasInvoked() + coVerify { arrangement.conversationFolderDAO.getFoldersWithConversations() }.wasInvoked() + } + private class Arrangement { @Mock @@ -197,5 +258,25 @@ class ConversationFolderRepositoryTest { coEvery { userPropertiesApi.setProperty(any(), any()) }.returns(response) return this } + + suspend fun withUpdateLabels(response: NetworkResponse): Arrangement { + coEvery { userPropertiesApi.updateLabels(any()) }.returns(response) + return this + } + + suspend fun withGetFoldersWithConversations(folders: List = emptyList()): Arrangement { + coEvery { conversationFolderDAO.getFoldersWithConversations() }.returns(folders.map { it.toDao() }) + return this + } + + suspend fun withAddConversationToFolder(): Arrangement { + coEvery { conversationFolderDAO.addConversationToFolder(any(), any()) }.returns(Unit) + return this + } + + suspend fun withRemoveConversationFromFolder(): Arrangement { + coEvery { conversationFolderDAO.removeConversationFromFolder(any(), any()) }.returns(Unit) + return this + } } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCaseTest.kt new file mode 100644 index 00000000000..ac1fb8fb5fc --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCaseTest.kt @@ -0,0 +1,131 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestFolder +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.eq +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class AddConversationToFavoritesUseCaseTest { + + @Test + fun givenValidConversation_WhenAddedToFavoritesSuccessfully_ThenReturnSuccess() = runTest { + val (arrangement, addConversationToFavoritesUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .withAddConversationToFolder(Either.Right(Unit)) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = addConversationToFavoritesUseCase(TestConversation.ID) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder( + eq(TestConversation.ID), + eq(TestFolder.FAVORITE.id) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversation_WhenFavoriteFolderNotFound_ThenReturnFailure() = runTest { + val (arrangement, addConversationToFavoritesUseCase) = Arrangement() + .withFavoriteFolder(Either.Left(CoreFailure.Unknown(null))) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = addConversationToFavoritesUseCase(TestConversation.ID) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversation_WhenAddToFolderFails_ThenReturnFailure() = runTest { + val (arrangement, addConversationToFavoritesUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .withAddConversationToFolder(Either.Left(CoreFailure.Unknown(null))) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = addConversationToFavoritesUseCase(TestConversation.ID) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder( + eq(TestConversation.ID), + eq(TestFolder.FAVORITE.id) + ) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val addConversationToFavoritesUseCase = AddConversationToFavoritesUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withFavoriteFolder(either: Either) = apply { + coEvery { + conversationFolderRepository.getFavoriteConversationFolder() + }.returns(either) + } + + suspend fun withAddConversationToFolder(either: Either) = apply { + coEvery { + conversationFolderRepository.addConversationToFolder(any(), any()) + }.returns(either) + } + + suspend fun withSyncConversationFoldersFromLocal(either: Either) = apply { + coEvery { + conversationFolderRepository.syncConversationFoldersFromLocal() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to addConversationToFavoritesUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCaseTest.kt new file mode 100644 index 00000000000..ca6fc8e156f --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCaseTest.kt @@ -0,0 +1,86 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase.Result +import com.wire.kalium.logic.framework.TestFolder +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class GetFavoriteFolderUseCaseTest { + + @Test + fun givenFavoriteFolderExists_WhenInvoked_ThenReturnSuccess() = runTest { + val (arrangement, getFavoriteFolderUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .arrange() + + val result = getFavoriteFolderUseCase() + + assertIs(result) + assertIs(result.folder) + assertEquals(TestFolder.FAVORITE.id, result.folder.id) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + } + + @Test + fun givenFavoriteFolderDoesNotExist_WhenInvoked_ThenReturnFailure() = runTest { + val (arrangement, getFavoriteFolderUseCase) = Arrangement() + .withFavoriteFolder(Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = getFavoriteFolderUseCase() + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val getFavoriteFolderUseCase = GetFavoriteFolderUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withFavoriteFolder(either: Either) = apply { + coEvery { + conversationFolderRepository.getFavoriteConversationFolder() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to getFavoriteFolderUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt new file mode 100644 index 00000000000..74d5abef8bc --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt @@ -0,0 +1,94 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.framework.TestConversationDetails +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ObserveConversationsFromFolderUseCaseTest { + + @Test + fun givenFolderId_WhenConversationsExist_ThenReturnFlowWithConversations() = runTest { + val testFolderId = "test-folder-id" + val testConversations = listOf( + TestConversationDetails.CONNECTION, + TestConversationDetails.CONVERSATION_ONE_ONE, + ).map { + ConversationDetailsWithEvents( + conversationDetails = it + ) + } + + val (arrangement, observeConversationsUseCase) = Arrangement() + .withConversationsFromFolder(testFolderId, testConversations) + .arrange() + + val result = observeConversationsUseCase(testFolderId).first() + + assertEquals(testConversations, result) + + coVerify { + arrangement.conversationFolderRepository.observeConversationsFromFolder(testFolderId) + }.wasInvoked(exactly = once) + } + + @Test + fun givenFolderId_WhenNoConversationsExist_ThenReturnEmptyFlow() = runTest { + val testFolderId = "test-folder-id" + + val (arrangement, observeConversationsUseCase) = Arrangement() + .withConversationsFromFolder(testFolderId, emptyList()) + .arrange() + + val result = observeConversationsUseCase(testFolderId).first() + + assertEquals(emptyList(), result) + + coVerify { + arrangement.conversationFolderRepository.observeConversationsFromFolder(testFolderId) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val observeConversationsFromFolderUseCase = ObserveConversationsFromFolderUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withConversationsFromFolder(folderId: String, conversationList: List) = apply { + coEvery { + conversationFolderRepository.observeConversationsFromFolder(folderId) + }.returns(flowOf(conversationList)) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to observeConversationsFromFolderUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCaseTest.kt new file mode 100644 index 00000000000..bbc81934ea3 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCaseTest.kt @@ -0,0 +1,134 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestFolder +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class RemoveConversationFromFavoritesUseCaseTest { + + @Test + fun givenValidConversation_WhenRemovedSuccessfullyFromFavorite_ThenReturnSuccess() = runTest { + val testConversationId = TestConversation.ID + + val (arrangement, removeConversationUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .withRemoveConversationFromFolder(testConversationId, TestFolder.FAVORITE.id, Either.Right(Unit)) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = removeConversationUseCase(testConversationId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, TestFolder.FAVORITE.id) + }.wasInvoked(exactly = once) + } + + @Test + fun givenInvalidConversation_WhenFavoriteFolderNotFound_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + + val (arrangement, removeConversationUseCase) = Arrangement() + .withFavoriteFolder(Either.Left(CoreFailure.Unknown(null))) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = removeConversationUseCase(testConversationId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversation_WhenRemoveFromFavoritesFails_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + + val (arrangement, removeConversationUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .withRemoveConversationFromFolder(testConversationId, TestFolder.FAVORITE.id, Either.Left(CoreFailure.Unknown(null))) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = removeConversationUseCase(testConversationId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, TestFolder.FAVORITE.id) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val removeConversationUseCase = RemoveConversationFromFavoritesUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withFavoriteFolder(either: Either) = apply { + coEvery { + conversationFolderRepository.getFavoriteConversationFolder() + }.returns(either) + } + + suspend fun withRemoveConversationFromFolder( + conversationId: ConversationId, + folderId: String, + either: Either + ) = apply { + coEvery { + conversationFolderRepository.removeConversationFromFolder(conversationId, folderId) + }.returns(either) + } + + suspend fun withSyncConversationFoldersFromLocal(either: Either) = apply { + coEvery { + conversationFolderRepository.syncConversationFoldersFromLocal() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to removeConversationUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCaseTest.kt new file mode 100644 index 00000000000..9a806ec5816 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCaseTest.kt @@ -0,0 +1,149 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.personaltoteamaccount + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.configuration.server.ServerConfigRepository +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO +import com.wire.kalium.network.session.SessionManager +import com.wire.kalium.network.utils.TestRequestHandler.Companion.TEST_BACKEND_CONFIG +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.every +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CanMigrateFromPersonalToTeamUseCaseTest { + + @Test + fun givenAPIVersionBelowMinimumAndUserNotInATeam_whenInvoking_thenReturnsFalse() = runTest { + // Given + val (_, useCase) = Arrangement() + .withRepositoryReturningMinimumApiVersion() + .withTeamId(Either.Right(null)) + .withServerConfig(6) + .arrange() + + // When + val result = useCase.invoke() + + // Then + assertFalse(result) + } + + @Test + fun givenAPIVersionEqualToMinimumAndUserNotInATeam_whenInvoking_thenReturnsTrue() = + runTest { + // Given + val (_, useCase) = Arrangement() + .withRepositoryReturningMinimumApiVersion() + .withServerConfig(7) + .withTeamId(Either.Right(null)) + .arrange() + + // When + val result = useCase.invoke() + + // Then + assertTrue(result) + } + + @Test + fun givenAPIVersionAboveMinimumAndUserInATeam_whenInvoking_thenReturnsFalse() = runTest { + // Given + val (_, useCase) = Arrangement() + .withRepositoryReturningMinimumApiVersion() + .withTeamId(Either.Right(TeamId("teamId"))) + .withServerConfig(9) + .arrange() + + // When + val result = useCase.invoke() + + // Then + assertFalse(result) + } + + + @Test + fun givenSelfTeamIdProviderFailure_whenInvoking_thenReturnsFalse() = runTest { + // Given + val (_, useCase) = Arrangement() + .withRepositoryReturningMinimumApiVersion() + .withTeamId(Either.Left(CoreFailure.MissingClientRegistration)) + .withServerConfig(9) + .arrange() + + // When + val result = useCase.invoke() + + // Then + assertFalse(result) + } + + private class Arrangement { + + @Mock + val serverConfigRepository = mock(ServerConfigRepository::class) + + @Mock + val sessionManager = mock(SessionManager::class) + + @Mock + val selfTeamIdProvider = mock(SelfTeamIdProvider::class) + + suspend fun withTeamId(result: Either) = apply { + coEvery { + selfTeamIdProvider() + }.returns(result) + } + + fun withRepositoryReturningMinimumApiVersion() = apply { + every { + serverConfigRepository.minimumApiVersionForPersonalToTeamAccountMigration + }.returns(MIN_API_VERSION) + } + + fun withServerConfig(apiVersion: Int) = apply { + val backendConfig = TEST_BACKEND_CONFIG.copy( + metaData = TEST_BACKEND_CONFIG.metaData.copy( + commonApiVersion = ApiVersionDTO.Valid(apiVersion) + ) + ) + every { + sessionManager.serverConfig() + }.returns(backendConfig) + } + + fun arrange() = this to CanMigrateFromPersonalToTeamUseCaseImpl( + sessionManager = sessionManager, + serverConfigRepository = serverConfigRepository, + selfTeamIdProvider = selfTeamIdProvider + ) + } + + companion object { + private const val MIN_API_VERSION = 7 + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt index 8641ac949cb..ecd484c1d2c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt @@ -176,7 +176,8 @@ object TestConversation { userSupportedProtocols = null, userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, - accentId = null + accentId = null, + isFavorite = false ) fun one_on_one(convId: ConversationId) = Conversation( @@ -341,7 +342,8 @@ object TestConversation { userSupportedProtocols = null, userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, - accentId = null + accentId = null, + isFavorite = false ) val MLS_PROTOCOL_INFO = ProtocolInfo.MLS( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestFolder.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestFolder.kt new file mode 100644 index 00000000000..e194be3b22e --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestFolder.kt @@ -0,0 +1,36 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.framework + +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.FolderType + +object TestFolder { + + val USER = ConversationFolder( + id = "folderId", + name = "friends", + type = FolderType.USER + ) + + val FAVORITE = ConversationFolder( + id = "favoriteFolderId", + name = "", + type = FolderType.FAVORITE + ) +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/properties/PropertiesApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/properties/PropertiesApi.kt index c224ec6b133..816278bbcda 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/properties/PropertiesApi.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/properties/PropertiesApi.kt @@ -27,5 +27,6 @@ interface PropertiesApi { suspend fun setProperty(propertyKey: PropertyKey, propertyValue: Any): NetworkResponse suspend fun deleteProperty(propertyKey: PropertyKey): NetworkResponse suspend fun getLabels(): NetworkResponse + suspend fun updateLabels(labelList: LabelListResponseDTO): NetworkResponse } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/PropertiesApiV0.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/PropertiesApiV0.kt index 7a556a9cf08..5601a5fa3c6 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/PropertiesApiV0.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/PropertiesApiV0.kt @@ -52,4 +52,8 @@ internal open class PropertiesApiV0 internal constructor( override suspend fun getLabels(): NetworkResponse = wrapKaliumResponse { httpClient.get("$PATH_PROPERTIES/$PATH_LABELS") } + + override suspend fun updateLabels(labelList: LabelListResponseDTO): NetworkResponse = wrapKaliumResponse { + httpClient.put("$PATH_PROPERTIES/$PATH_LABELS") { setBody(labelList) } + } } diff --git a/persistence/src/commonMain/db_global/com/wire/kalium/persistence/ServerConfiguration.sq b/persistence/src/commonMain/db_global/com/wire/kalium/persistence/ServerConfiguration.sq index b09efc6e29a..48a8d2fe6fe 100644 --- a/persistence/src/commonMain/db_global/com/wire/kalium/persistence/ServerConfiguration.sq +++ b/persistence/src/commonMain/db_global/com/wire/kalium/persistence/ServerConfiguration.sq @@ -35,6 +35,9 @@ UPDATE ServerConfiguration SET commonApiVersion = ? WHERE id = ?; updateApiVersionAndDomain: UPDATE ServerConfiguration SET commonApiVersion = ?, domain = ? WHERE id = ?; +getCommonApiVersionByDomain: +SELECT commonApiVersion FROM ServerConfiguration WHERE domain = ?; + updateLastBlackListCheckByIds: UPDATE ServerConfiguration SET lastBlackListCheck = ? WHERE id IN ?; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq index 8cab007519e..66403d5116a 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq @@ -108,7 +108,8 @@ CASE ELSE 1 END ELSE 0 -END AS interactionEnabled +END AS interactionEnabled, +LabeledConversation.folder_id IS NOT NULL AS isFavorite FROM Conversation LEFT JOIN SelfUser LEFT JOIN Member ON Conversation.qualified_id = Member.conversation @@ -123,7 +124,9 @@ LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualifi OR Connection.status = 'NOT_CONNECTED' AND Conversation.type IS 'CONNECTION_PENDING') LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id -LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1); +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1) +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type = 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id; selectAllConversationDetails: SELECT * FROM ConversationDetails diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq index 724326e53ba..e1e5d8f14cb 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq @@ -17,6 +17,19 @@ CREATE TABLE LabeledConversation ( PRIMARY KEY (folder_id, conversation_id) ); +getAllFoldersWithConversations: +SELECT + conversationFolder.id AS label_id, + conversationFolder.name AS label_name, + conversationFolder.folder_type AS label_type, + labeledConversation.conversation_id +FROM + ConversationFolder conversationFolder +LEFT JOIN + LabeledConversation labeledConversation ON conversationFolder.id = labeledConversation.folder_id +ORDER BY + conversationFolder.id; + getConversationsFromFolder: SELECT ConversationDetailsWithEvents.* FROM LabeledConversation @@ -44,5 +57,8 @@ insertLabeledConversation: INSERT OR IGNORE INTO LabeledConversation(conversation_id, folder_id) VALUES(?, ?); +deleteLabeledConversation: +DELETE FROM LabeledConversation WHERE conversation_id = ? AND folder_id = ?; + clearFolders: DELETE FROM ConversationFolder; diff --git a/persistence/src/commonMain/db_user/migrations/92.sqm b/persistence/src/commonMain/db_user/migrations/92.sqm new file mode 100644 index 00000000000..3f36fd16814 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/92.sqm @@ -0,0 +1,131 @@ +DROP VIEW IF EXISTS ConversationDetails; + +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE + WHEN (SelfUser.id LIKE (Conversation.creator_id || '@%')) THEN 1 + ELSE 0 +END AS isCreator, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled, +LabeledConversation.folder_id IS NOT NULL AS isFavorite +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1) +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type = 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt index 4f76e77ffad..8e77a9aa642 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt @@ -80,6 +80,7 @@ data object ConversationDetailsWithEventsMapper { legalHoldStatus: ConversationEntity.LegalHoldStatus, selfUserId: QualifiedIDEntity?, interactionEnabled: Long, + isFavorite: Boolean, unreadKnocksCount: Long?, unreadMissedCallsCount: Long?, unreadMentionsCount: Long?, @@ -154,6 +155,7 @@ data object ConversationDetailsWithEventsMapper { legalHoldStatus = legalHoldStatus, selfUserId = selfUserId, interactionEnabled = interactionEnabled, + isFavorite = isFavorite ), unreadEvents = UnreadEventMapper.toConversationUnreadEntity( conversationId = qualifiedId, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt index 8cea5874e4a..d421e6b0de0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt @@ -76,6 +76,7 @@ data object ConversationMapper { legalHoldStatus: ConversationEntity.LegalHoldStatus, selfUserId: QualifiedIDEntity?, interactionEnabled: Long, + isFavorite: Boolean, ): ConversationViewEntity = ConversationViewEntity( id = qualifiedId, name = name, @@ -125,7 +126,8 @@ data object ConversationMapper { userActiveOneOnOneConversationId = otherUserActiveConversationId, proteusVerificationStatus = proteusVerificationStatus, legalHoldStatus = legalHoldStatus, - accentId = accentId + accentId = accentId, + isFavorite = isFavorite ) @Suppress("LongParameterList", "UnusedParameter") @@ -161,7 +163,7 @@ data object ConversationMapper { verificationStatus: ConversationEntity.VerificationStatus, proteusVerificationStatus: ConversationEntity.VerificationStatus, degradedConversationNotified: Boolean, - legalHoldStatus: ConversationEntity.LegalHoldStatus, + legalHoldStatus: ConversationEntity.LegalHoldStatus ) = ConversationEntity( id = qualifiedId, name = name, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt index f38a26082e5..d9ebd64303b 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt @@ -74,6 +74,7 @@ data class ConversationViewEntity( val proteusVerificationStatus: ConversationEntity.VerificationStatus, val legalHoldStatus: ConversationEntity.LegalHoldStatus, val accentId: Int?, + val isFavorite: Boolean, ) { val isMember: Boolean get() = selfRole != null } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt index 8c0e6f2ac84..ed1ae82a9f8 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt @@ -17,11 +17,15 @@ */ package com.wire.kalium.persistence.dao.conversation.folder +import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import kotlinx.coroutines.flow.Flow interface ConversationFolderDAO { + suspend fun getFoldersWithConversations(): List suspend fun observeConversationListFromFolder(folderId: String): Flow> suspend fun getFavoriteConversationFolder(): ConversationFolderEntity suspend fun updateConversationFolders(folderWithConversationsList: List) + suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) + suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt index 06768bf0110..3c19bd871b3 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt @@ -19,6 +19,8 @@ package com.wire.kalium.persistence.dao.conversation.folder import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationFoldersQueries +import com.wire.kalium.persistence.GetAllFoldersWithConversations +import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsMapper import com.wire.kalium.persistence.util.mapToList @@ -33,6 +35,30 @@ class ConversationFolderDAOImpl internal constructor( ) : ConversationFolderDAO { private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper + override suspend fun getFoldersWithConversations(): List = withContext(coroutineContext) { + val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().executeAsList().map(::toEntity) + + val folderMap = labeledConversationList.groupBy { it.folderId }.mapValues { entry -> + val folderId = entry.key + val firstRow = entry.value.first() + FolderWithConversationsEntity( + id = folderId, + name = firstRow.folderName, + type = firstRow.folderType, + conversationIdList = entry.value.mapNotNull { it.conversationId } + ) + } + + folderMap.values.toList() + } + + private fun toEntity(row: GetAllFoldersWithConversations) = LabeledConversationEntity( + folderId = row.label_id, + folderName = row.label_name, + folderType = row.label_type, + conversationId = row.conversation_id + ) + override suspend fun observeConversationListFromFolder(folderId: String): Flow> { return conversationFoldersQueries.getConversationsFromFolder( folderId, @@ -52,7 +78,6 @@ class ConversationFolderDAOImpl internal constructor( override suspend fun updateConversationFolders(folderWithConversationsList: List) = withContext(coroutineContext) { - // TODO KBX make it better to not have blinking effect on favorites list conversationFoldersQueries.transaction { conversationFoldersQueries.clearFolders() folderWithConversationsList.forEach { folderWithConversations -> @@ -70,4 +95,12 @@ class ConversationFolderDAOImpl internal constructor( } } } + + override suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) = withContext(coroutineContext) { + conversationFoldersQueries.insertLabeledConversation(conversationId, folderId) + } + + override suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) = withContext(coroutineContext) { + conversationFoldersQueries.deleteLabeledConversation(conversationId, folderId) + } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt index 1bad61fbd08..77ceccda8f5 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt @@ -32,6 +32,13 @@ data class FolderWithConversationsEntity( val conversationIdList: List ) +data class LabeledConversationEntity( + val folderId: String, + val folderName: String, + val folderType: ConversationFolderTypeEntity, + val conversationId: QualifiedIDEntity? +) + enum class ConversationFolderTypeEntity { USER, FAVORITE diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt index 707e857eb1c..7774eae162a 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt @@ -126,6 +126,7 @@ interface ServerConfigurationDAO { fun configById(id: String): ServerConfigEntity? suspend fun configByLinks(links: ServerConfigEntity.Links): ServerConfigEntity? suspend fun updateApiVersion(id: String, commonApiVersion: Int) + suspend fun getCommonApiVersion(domain: String): Int suspend fun updateApiVersionAndDomain(id: String, domain: String, commonApiVersion: Int) suspend fun configForUser(userId: UserIDEntity): ServerConfigEntity? suspend fun setFederationToTrue(id: String) @@ -213,6 +214,10 @@ internal class ServerConfigurationDAOImpl internal constructor( queries.updateApiVersion(commonApiVersion, id) } + override suspend fun getCommonApiVersion(domain: String): Int = withContext(queriesContext) { + queries.getCommonApiVersionByDomain(domain).executeAsOne() + } + override suspend fun updateApiVersionAndDomain(id: String, domain: String, commonApiVersion: Int) = withContext(queriesContext) { queries.updateApiVersionAndDomain(commonApiVersion, domain, id) diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt index 6bb242bf28d..96e992c328c 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt @@ -2384,7 +2384,8 @@ class ConversationDAOTest : BaseDatabaseTest() { userSupportedProtocols = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.supportedProtocols else null, userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, - accentId = 1 + accentId = 1, + isFavorite = false ) } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt index 273c1da825b..c55255dd733 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue class ConversationFolderDAOTest : BaseDatabaseTest() { @@ -86,6 +87,126 @@ class ConversationFolderDAOTest : BaseDatabaseTest() { assertEquals(folderId, result.id) } + @Test + fun givenMultipleFolders_whenRetrievingFolders_shouldReturnCorrectData() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folder1 = folderWithConversationsEntity( + id = "folderId1", + name = "Folder 1", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + val folder2 = folderWithConversationsEntity( + id = "folderId2", + name = "Folder 2", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf() + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder1, folder2)) + val result = db.conversationFolderDAO.getFoldersWithConversations() + + assertEquals(2, result.size) + assertTrue(result.any { it.id == "folderId1" && it.name == "Folder 1" }) + assertTrue(result.any { it.id == "folderId2" && it.name == "Folder 2" }) + } + + @Test + fun givenFolderWithConversation_whenRemovingConversation_thenFolderShouldBeEmpty() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folderId = "folderId1" + val folder = folderWithConversationsEntity( + id = folderId, + name = "Test Folder", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder)) + db.conversationFolderDAO.removeConversationFromFolder(conversationEntity1.id, folderId) + + val result = db.conversationFolderDAO.observeConversationListFromFolder(folderId).first() + + assertTrue(result.isEmpty()) + } + + @Test + fun givenFolderWithConversations_whenDeletingFolder_thenFolderShouldBeRemoved() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folder = folderWithConversationsEntity( + id = "folderId1", + name = "Folder 1", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder)) + db.conversationFolderDAO.updateConversationFolders(listOf()) // Clear folders + + val result = db.conversationFolderDAO.getFoldersWithConversations() + + assertTrue(result.isEmpty()) + } + + @Test + fun givenEmptyFolder_whenAddingToDatabase_thenShouldBeRetrievable() = runTest { + val folder = folderWithConversationsEntity( + id = "folderId1", + name = "Empty Folder", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf() + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder)) + + val result = db.conversationFolderDAO.getFoldersWithConversations() + + assertEquals(1, result.size) + assertEquals("folderId1", result.first().id) + assertTrue(result.first().conversationIdList.isEmpty()) + } + + @Test + fun givenConversationAddedToUserAndFavoriteFolders_whenRetrievingFolders_thenShouldBeInBothFolders() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val userFolder = folderWithConversationsEntity( + id = "userFolderId", + name = "User Folder", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + val favoriteFolder = folderWithConversationsEntity( + id = "favoriteFolderId", + name = "Favorites", + type = ConversationFolderTypeEntity.FAVORITE, + conversationIdList = listOf(conversationEntity1.id) + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(userFolder, favoriteFolder)) + + val userFolderResult = db.conversationFolderDAO.observeConversationListFromFolder("userFolderId").first() + assertEquals(1, userFolderResult.size) + assertEquals(conversationEntity1.id, userFolderResult.first().conversationViewEntity.id) + + val favoriteFolderResult = db.conversationFolderDAO.observeConversationListFromFolder("favoriteFolderId").first() + assertEquals(1, favoriteFolderResult.size) + assertEquals(conversationEntity1.id, favoriteFolderResult.first().conversationViewEntity.id) + } + companion object { fun folderWithConversationsEntity( id: String = "folderId",