From 4362a2300c265ad69d9d86756ce9099953035b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Mon, 13 Jan 2025 13:05:46 +0100 Subject: [PATCH] feat: move to folder [WPB-14627] (#3213) * feat: move conversation to folder * feat: move to folder use case * refactor and added tests * tests fix --- .../logic/data/conversation/Conversation.kt | 6 +- .../data/conversation/ConversationMapper.kt | 6 +- .../folders/ConversationFolderRepository.kt | 6 +- .../feature/conversation/ConversationScope.kt | 4 + .../folder/MoveConversationToFolderUseCase.kt | 80 ++++++++ .../folder/ObserveUserFoldersUseCase.kt | 12 +- .../MoveConversationToFolderUseCaseTest.kt | 175 ++++++++++++++++++ .../folder/ObserveUserFoldersUseCaseTest.kt | 148 +++++++++++++++ .../logic/framework/TestConversation.kt | 8 +- .../kalium/persistence/ConversationDetails.sq | 10 +- .../kalium/persistence/ConversationFolders.sq | 5 +- .../src/commonMain/db_user/migrations/95.sqm | 131 +++++++++++++ .../ConversationDetailsWithEventsMapper.kt | 6 +- .../dao/conversation/ConversationMapper.kt | 6 +- .../conversation/ConversationViewEntity.kt | 2 + .../folder/ConversationFolderDAO.kt | 2 +- .../folder/ConversationFolderDAOImpl.kt | 4 +- .../persistence/dao/ConversationDAOTest.kt | 4 +- 18 files changed, 593 insertions(+), 22 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCase.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCaseTest.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCaseTest.kt create mode 100644 persistence/src/commonMain/db_user/migrations/95.sqm 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 b07cf80f009..20ba492df9f 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 @@ -307,7 +307,8 @@ sealed class ConversationDetails(open val conversation: Conversation) { override val conversation: Conversation, val otherUser: OtherUser, val userType: UserType, - val isFavorite: Boolean = false + val isFavorite: Boolean = false, + val folder: ConversationFolder? = null ) : ConversationDetails(conversation) data class Group( @@ -315,7 +316,8 @@ sealed class ConversationDetails(open val conversation: Conversation) { val hasOngoingCall: Boolean = false, val isSelfUserMember: Boolean, val selfRole: Conversation.Member.Role?, - val isFavorite: Boolean = false + val isFavorite: Boolean = false, + val folder: ConversationFolder? = null // val isTeamAdmin: Boolean, TODO kubaz ) : ConversationDetails(conversation) 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 8ec1b717167..38c398ef667 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,7 +263,8 @@ internal class ConversationMapperImpl( activeOneOnOneConversationId = userActiveOneOnOneConversationId?.toModel() ), userType = domainUserTypeMapper.fromUserTypeEntity(userType), - isFavorite = isFavorite + isFavorite = isFavorite, + folder = folderId?.let { ConversationFolder(it, folderName ?: "", type = FolderType.USER) }, ) } @@ -273,7 +274,8 @@ internal class ConversationMapperImpl( hasOngoingCall = callStatus != null, // todo: we can do better! isSelfUserMember = isMember, selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, - isFavorite = isFavorite + isFavorite = isFavorite, + folder = folderId?.let { ConversationFolder(it, folderName ?: "", type = FolderType.USER) }, ) } 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 5db72788cbd..d92c71317b8 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 @@ -58,7 +58,7 @@ internal interface ConversationFolderRepository { suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either suspend fun syncConversationFoldersFromLocal(): Either - suspend fun observeUserFolders(): Flow>> + suspend fun observeFolders(): Flow>> } internal class ConversationFolderDataSource internal constructor( @@ -155,8 +155,8 @@ internal class ConversationFolderDataSource internal constructor( } } - override suspend fun observeUserFolders(): Flow>> { - return conversationFolderDAO.observeUserFolders() + override suspend fun observeFolders(): Flow>> { + return conversationFolderDAO.observeFolders() .wrapStorageRequest() .mapRight { folderEntities -> folderEntities.map { it.toModel() } } } 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 39839498275..842a6765fc8 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 @@ -55,6 +55,8 @@ import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavori 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.MoveConversationToFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCaseImpl 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.ObserveUserFoldersUseCase @@ -386,4 +388,6 @@ class ConversationScope internal constructor( get() = RemoveConversationFromFavoritesUseCaseImpl(conversationFolderRepository) val observeUserFolders: ObserveUserFoldersUseCase get() = ObserveUserFoldersUseCaseImpl(conversationFolderRepository) + val moveConversationToFolder: MoveConversationToFolderUseCase + get() = MoveConversationToFolderUseCaseImpl(conversationFolderRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCase.kt new file mode 100644 index 00000000000..7ee1e1fbcd7 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCase.kt @@ -0,0 +1,80 @@ +/* + * 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.Either +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 move a conversation to the selected folder. + */ +interface MoveConversationToFolderUseCase { + /** + * @param conversationId the id of the conversation + * @param folderId the id of the conversation folder + * @param previousFolderId the id of the previous folder, if any + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke( + conversationId: ConversationId, + folderId: String, + previousFolderId: String? + ): Result + + sealed interface Result { + data object Success : Result + data class Failure(val cause: CoreFailure) : Result + } +} + +internal class MoveConversationToFolderUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : MoveConversationToFolderUseCase { + override suspend fun invoke( + conversationId: ConversationId, + folderId: String, + previousFolderId: String? + ): MoveConversationToFolderUseCase.Result = withContext(dispatchers.io) { + ( + previousFolderId?.let { + conversationFolderRepository.removeConversationFromFolder(conversationId, it) + } ?: Either.Right(Unit) + ) + .flatMap { + conversationFolderRepository.addConversationToFolder( + conversationId, + folderId + ) + } + .flatMap { conversationFolderRepository.syncConversationFoldersFromLocal() } + .fold({ + MoveConversationToFolderUseCase.Result.Failure(it) + }, { + MoveConversationToFolderUseCase.Result.Success + }) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt index 3935cd2af28..f0399efbca5 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt @@ -18,7 +18,9 @@ package com.wire.kalium.logic.feature.conversation.folder import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.FolderType import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.functional.mapRight import com.wire.kalium.logic.functional.mapToRightOr import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl @@ -39,7 +41,15 @@ internal class ObserveUserFoldersUseCaseImpl( ) : ObserveUserFoldersUseCase { override suspend operator fun invoke(): Flow> { - return conversationFolderRepository.observeUserFolders() + return conversationFolderRepository.observeFolders() + .mapRight { folders -> + if (folders.isEmpty()) { + conversationFolderRepository.fetchConversationFolders() + emptyList() + } else { + folders.filter { it.type == FolderType.USER } + } + } .mapToRightOr(emptyList()) .flowOn(dispatchers.io) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCaseTest.kt new file mode 100644 index 00000000000..eec942bab2c --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/MoveConversationToFolderUseCaseTest.kt @@ -0,0 +1,175 @@ +/* + * Wire + * Copyright (C) 2025 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.feature.conversation.folder.MoveConversationToFolderUseCase.Result +import com.wire.kalium.logic.framework.TestConversation +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 MoveConversationToFolderUseCaseTest { + + @Test + fun givenValidConversationAndFolder_WhenMoveIsSuccessful_ThenReturnSuccess() = runTest { + val testConversationId = TestConversation.ID + val testFolderId = "test-folder-id" + val previousFolderId = "previous-folder-id" + + val (arrangement, moveConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, previousFolderId, Either.Right(Unit)) + .withAddConversationToFolder(testConversationId, testFolderId, Either.Right(Unit)) + .withSyncFolders(Either.Right(Unit)) + .arrange() + + val result = moveConversationUseCase(testConversationId, testFolderId, previousFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, previousFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.syncConversationFoldersFromLocal() + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversationAndFolder_WhenRemoveFails_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + val testFolderId = "test-folder-id" + val previousFolderId = "previous-folder-id" + + val (arrangement, moveConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, previousFolderId, Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = moveConversationUseCase(testConversationId, testFolderId, previousFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, previousFolderId) + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversationAndFolder_WhenAddFails_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + val testFolderId = "test-folder-id" + val previousFolderId = "previous-folder-id" + + val (arrangement, moveConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, previousFolderId, Either.Right(Unit)) + .withAddConversationToFolder(testConversationId, testFolderId, Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = moveConversationUseCase(testConversationId, testFolderId, previousFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, previousFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversationAndFolder_WhenSyncFails_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + val testFolderId = "test-folder-id" + val previousFolderId = "previous-folder-id" + + val (arrangement, moveConversationUseCase) = Arrangement() + .withRemoveConversationFromFolder(testConversationId, previousFolderId, Either.Right(Unit)) + .withAddConversationToFolder(testConversationId, testFolderId, Either.Right(Unit)) + .withSyncFolders(Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = moveConversationUseCase(testConversationId, testFolderId, previousFolderId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, previousFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder(testConversationId, testFolderId) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.syncConversationFoldersFromLocal() + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val moveConversationToFolderUseCase = MoveConversationToFolderUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withRemoveConversationFromFolder( + conversationId: ConversationId, + folderId: String, + either: Either + ) = apply { + coEvery { + conversationFolderRepository.removeConversationFromFolder(conversationId, folderId) + }.returns(either) + } + + suspend fun withAddConversationToFolder( + conversationId: ConversationId, + folderId: String, + either: Either + ) = apply { + coEvery { + conversationFolderRepository.addConversationToFolder(conversationId, folderId) + }.returns(either) + } + + suspend fun withSyncFolders(either: Either) = apply { + coEvery { + conversationFolderRepository.syncConversationFoldersFromLocal() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to moveConversationToFolderUseCase } + } +} + diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCaseTest.kt new file mode 100644 index 00000000000..690c2d3af32 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCaseTest.kt @@ -0,0 +1,148 @@ +/* + * Wire + * Copyright (C) 2025 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.FolderType +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +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.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ObserveUserFoldersUseCaseTest { + + @Test + fun givenFoldersExist_WhenObserved_ThenReturnUserFolders() = runTest { + val userFolders = listOf( + ConversationFolder("1", "Custom Folder", FolderType.USER), + ConversationFolder("2", "Second Folder", FolderType.USER) + ) + val allFolders = userFolders + ConversationFolder("3", "", FolderType.FAVORITE) + + val (arrangement, observeUserFoldersUseCase) = Arrangement() + .withObserveFolders(flowOf(Either.Right(allFolders))) + .arrange() + + val result = observeUserFoldersUseCase().first() + + assertEquals(userFolders, result) + + coVerify { + arrangement.conversationFolderRepository.observeFolders() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.fetchConversationFolders() + }.wasNotInvoked() + } + + @Test + fun givenNoFoldersExist_WhenObserved_ThenFetchFoldersAndReturnEmptyList() = runTest { + val (arrangement, observeUserFoldersUseCase) = Arrangement() + .withObserveFolders(flowOf(Either.Right(emptyList()))) + .withFetchConversationFolders(Either.Right(Unit)) + .arrange() + + val result = observeUserFoldersUseCase().first() + + assertEquals(emptyList(), result) + + coVerify { + arrangement.conversationFolderRepository.observeFolders() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.fetchConversationFolders() + }.wasInvoked(exactly = once) + } + + @Test + fun givenOnlyFavoriteFoldersExist_WhenObserved_ThenDoNotFetchFoldersAndReturnEmptyList() = runTest { + val favoriteFolders = listOf( + ConversationFolder("1", "Favorites", FolderType.FAVORITE), + ) + + val (arrangement, observeUserFoldersUseCase) = Arrangement() + .withObserveFolders(flowOf(Either.Right(favoriteFolders))) + .arrange() + + val result = observeUserFoldersUseCase().first() + + assertEquals(emptyList(), result) + + coVerify { + arrangement.conversationFolderRepository.observeFolders() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.fetchConversationFolders() + }.wasNotInvoked() + } + + @Test + fun givenErrorInObserveFolders_WhenObserved_ThenReturnEmptyList() = runTest { + val (arrangement, observeUserFoldersUseCase) = Arrangement() + .withObserveFolders(flowOf(Either.Left(CoreFailure.Unknown(null)))) + .arrange() + + val result = observeUserFoldersUseCase().first() + + assertEquals(emptyList(), result) + + coVerify { + arrangement.conversationFolderRepository.observeFolders() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.fetchConversationFolders() + }.wasNotInvoked() + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val observeUserFoldersUseCase = ObserveUserFoldersUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withObserveFolders(flow: Flow>>) = apply { + coEvery { + conversationFolderRepository.observeFolders() + }.returns(flow) + } + + suspend fun withFetchConversationFolders(either: Either) = apply { + coEvery { + conversationFolderRepository.fetchConversationFolders() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to observeUserFoldersUseCase } + } +} 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 6f36299004d..3193d607bf7 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,9 @@ object TestConversation { userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, accentId = null, - isFavorite = false + isFavorite = false, + folderId = null, + folderName = null ) fun one_on_one(convId: ConversationId) = Conversation( @@ -341,7 +343,9 @@ object TestConversation { userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, accentId = null, - isFavorite = false + isFavorite = false, + folderId = null, + folderName = null ) val MLS_PROTOCOL_INFO = ProtocolInfo.MLS( 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 b521bb3c216..ca9da4c0efa 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 @@ -105,7 +105,9 @@ CASE END ELSE 0 END AS interactionEnabled, -LabeledConversation.folder_id IS NOT NULL AS isFavorite +LabeledConversation.folder_id IS NOT NULL AS isFavorite, +CurrentFolder.id AS folderId, +CurrentFolder.name AS folderName FROM Conversation LEFT JOIN SelfUser LEFT JOIN Member ON Conversation.qualified_id = Member.conversation @@ -121,8 +123,10 @@ LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualifi 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; +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type IS 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id +LEFT JOIN LabeledConversation AS ConversationLabel ON ConversationLabel.conversation_id = Conversation.qualified_id AND ConversationLabel.folder_id IS NOT FavoriteFolder.id +LEFT JOIN ConversationFolder AS CurrentFolder ON CurrentFolder.id = ConversationLabel.folder_id AND CurrentFolder.folder_type IS NOT 'FAVORITE'; 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 38d2c6db4bf..b5b165d38d7 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,9 +17,8 @@ CREATE TABLE LabeledConversation ( PRIMARY KEY (folder_id, conversation_id) ); -getUserFolders: -SELECT * FROM ConversationFolder -WHERE folder_type != 'FAVORITE'; +getFolders: +SELECT * FROM ConversationFolder; getAllFoldersWithConversations: SELECT diff --git a/persistence/src/commonMain/db_user/migrations/95.sqm b/persistence/src/commonMain/db_user/migrations/95.sqm new file mode 100644 index 00000000000..1a5eee4e177 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/95.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 (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, +CurrentFolder.id AS folderId, +CurrentFolder.name AS folderName +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 IS 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id +LEFT JOIN LabeledConversation AS ConversationLabel ON ConversationLabel.conversation_id = Conversation.qualified_id AND ConversationLabel.folder_id IS NOT FavoriteFolder.id +LEFT JOIN ConversationFolder AS CurrentFolder ON CurrentFolder.id = ConversationLabel.folder_id AND CurrentFolder.folder_type IS NOT 'FAVORITE'; 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 d59c32c6c2f..d2fdbad9b30 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,8 @@ data object ConversationDetailsWithEventsMapper { selfUserId: QualifiedIDEntity?, interactionEnabled: Long, isFavorite: Boolean, + folderId: String?, + folderName: String?, unreadKnocksCount: Long?, unreadMissedCallsCount: Long?, unreadMentionsCount: Long?, @@ -153,7 +155,9 @@ data object ConversationDetailsWithEventsMapper { legalHoldStatus = legalHoldStatus, selfUserId = selfUserId, interactionEnabled = interactionEnabled, - isFavorite = isFavorite + isFavorite = isFavorite, + folderId = folderId, + folderName = folderName, ), 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 4cb050db21d..9c87536de48 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,8 @@ data object ConversationMapper { selfUserId: QualifiedIDEntity?, interactionEnabled: Long, isFavorite: Boolean, + folderId: String?, + folderName: String?, ): ConversationViewEntity = ConversationViewEntity( id = qualifiedId, name = name, @@ -125,7 +127,9 @@ data object ConversationMapper { proteusVerificationStatus = proteusVerificationStatus, legalHoldStatus = legalHoldStatus, accentId = accentId, - isFavorite = isFavorite + isFavorite = isFavorite, + folderId = folderId, + folderName = folderName ) @Suppress("LongParameterList", "UnusedParameter") 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 8e5e247226e..e6a8fad9306 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,8 @@ data class ConversationViewEntity( val legalHoldStatus: ConversationEntity.LegalHoldStatus, val accentId: Int?, val isFavorite: Boolean, + val folderId: String?, + val folderName: String?, ) { 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 0c201de5b8d..463aae0e637 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 @@ -28,5 +28,5 @@ interface ConversationFolderDAO { suspend fun updateConversationFolders(folderWithConversationsList: List) suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) - suspend fun observeUserFolders(): Flow> + suspend fun observeFolders(): Flow> } 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 6e703b82419..7bd01e0e57b 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 @@ -37,8 +37,8 @@ class ConversationFolderDAOImpl internal constructor( ) : ConversationFolderDAO { private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper - override suspend fun observeUserFolders(): Flow> { - return conversationFoldersQueries.getUserFolders() + override suspend fun observeFolders(): Flow> { + return conversationFoldersQueries.getFolders() .asFlow() .mapToList() .map { it.map(::toEntity) } 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 a000851a0d6..b8134bc780b 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 @@ -2369,7 +2369,9 @@ class ConversationDAOTest : BaseDatabaseTest() { userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, accentId = 1, - isFavorite = false + isFavorite = false, + folderName = null, + folderId = null ) }