From 26b7d4b4dcf2b50b64a3979e6211094a7a5d63d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Tue, 10 Dec 2024 11:34:56 +0100 Subject: [PATCH] feat: user folders [WPB-14442] (#3147) * feat: user folders * detekt fix * removed duplicated sq query * fix: favorite folder error handling, non paginatated conversations from folder * tests fix --- .../data/conversation/ConversationFilter.kt | 23 ++++++++-- .../data/conversation/ConversationFolder.kt | 9 ++-- .../data/conversation/ConversationMapper.kt | 17 +++---- .../conversation/ConversationRepository.kt | 4 +- .../ConversationRepositoryExtensions.kt | 2 +- .../folders/ConversationFolderRepository.kt | 10 +++- .../feature/conversation/ConversationScope.kt | 4 ++ ...rsationListDetailsWithEventsUseCaseImpl.kt | 24 +++++++--- .../ObserveConversationsFromFolderUseCase.kt | 10 ++-- .../folder/ObserveUserFoldersUseCase.kt | 46 +++++++++++++++++++ .../kalium/persistence/ConversationFolders.sq | 10 ++-- .../folder/ConversationFolderDAO.kt | 3 +- .../folder/ConversationFolderDAOImpl.kt | 20 +++++++- .../folder/ConversationFolderDAOTest.kt | 8 ++-- 14 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt index f1ca2a7bf39..a76fac75ca0 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt @@ -17,9 +17,22 @@ */ package com.wire.kalium.logic.data.conversation -enum class ConversationFilter { - ALL, - FAVORITES, - GROUPS, - ONE_ON_ONE +import kotlinx.serialization.Serializable + +@Serializable +sealed class ConversationFilter { + @Serializable + data object All : ConversationFilter() + + @Serializable + data object Favorites : ConversationFilter() + + @Serializable + data object Groups : ConversationFilter() + + @Serializable + data object OneOnOne : ConversationFilter() + + @Serializable + data class Folder(val folderName: String, val folderId: String) : ConversationFilter() } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt index 675e9f5794f..cf64352dd3d 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt @@ -18,11 +18,14 @@ package com.wire.kalium.logic.data.conversation import com.wire.kalium.logic.data.id.QualifiedID +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable data class ConversationFolder( - val id: String, - val name: String, - val type: FolderType + @SerialName("id") val id: String, + @SerialName("name") val name: String, + @SerialName("folder_type") val type: FolderType ) data class FolderWithConversations( 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 cc2d9995a75..6382559ee23 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 @@ -676,15 +676,16 @@ internal fun ConversationEntity.VerificationStatus.toModel(): Conversation.Verif } internal fun ConversationFilter.toDao(): ConversationFilterEntity = when (this) { - ConversationFilter.ALL -> ConversationFilterEntity.ALL - ConversationFilter.FAVORITES -> ConversationFilterEntity.FAVORITES - ConversationFilter.GROUPS -> ConversationFilterEntity.GROUPS - ConversationFilter.ONE_ON_ONE -> ConversationFilterEntity.ONE_ON_ONE + ConversationFilter.All -> ConversationFilterEntity.ALL + ConversationFilter.Favorites -> ConversationFilterEntity.FAVORITES + ConversationFilter.Groups -> ConversationFilterEntity.GROUPS + ConversationFilter.OneOnOne -> ConversationFilterEntity.ONE_ON_ONE + is ConversationFilter.Folder -> ConversationFilterEntity.ALL // TODO think how to secure that } internal fun ConversationFilterEntity.toModel(): ConversationFilter = when (this) { - ConversationFilterEntity.ALL -> ConversationFilter.ALL - ConversationFilterEntity.FAVORITES -> ConversationFilter.FAVORITES - ConversationFilterEntity.GROUPS -> ConversationFilter.GROUPS - ConversationFilterEntity.ONE_ON_ONE -> ConversationFilter.ONE_ON_ONE + ConversationFilterEntity.ALL -> ConversationFilter.All + ConversationFilterEntity.FAVORITES -> ConversationFilter.Favorites + ConversationFilterEntity.GROUPS -> ConversationFilter.Groups + ConversationFilterEntity.ONE_ON_ONE -> ConversationFilter.OneOnOne } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt index f199fe862f7..2508b24762a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt @@ -132,12 +132,12 @@ interface ConversationRepository { suspend fun observeConversationList(): Flow> suspend fun observeConversationListDetails( fromArchive: Boolean, - conversationFilter: ConversationFilter = ConversationFilter.ALL + conversationFilter: ConversationFilter = ConversationFilter.All ): Flow> suspend fun observeConversationListDetailsWithEvents( fromArchive: Boolean = false, - conversationFilter: ConversationFilter = ConversationFilter.ALL + conversationFilter: ConversationFilter = ConversationFilter.All ): Flow> suspend fun getConversationIds( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt index aec18932a97..c41464ea225 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt @@ -73,5 +73,5 @@ data class ConversationQueryConfig( val fromArchive: Boolean = false, val onlyInteractionEnabled: Boolean = false, val newActivitiesOnTop: Boolean = false, - val conversationFilter: ConversationFilter = ConversationFilter.ALL, + val conversationFilter: ConversationFilter = ConversationFilter.All, ) 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 70ad373bdfa..5db72788cbd 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 @@ -34,6 +34,7 @@ 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.mapRight import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.functional.onSuccess import com.wire.kalium.logic.kaliumLogger @@ -57,6 +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>> } internal class ConversationFolderDataSource internal constructor( @@ -72,7 +74,7 @@ internal class ConversationFolderDataSource internal constructor( } override suspend fun getFavoriteConversationFolder(): Either = wrapStorageRequest { - conversationFolderDAO.getFavoriteConversationFolder().toModel() + conversationFolderDAO.getFavoriteConversationFolder()?.toModel() } override suspend fun observeConversationsFromFolder(folderId: String): Flow> = @@ -152,4 +154,10 @@ internal class ConversationFolderDataSource internal constructor( } } } + + override suspend fun observeUserFolders(): Flow>> { + return conversationFolderDAO.observeUserFolders() + .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 0f3be756ff3..bc65552ad2d 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.GetFavoriteFolderUseCas 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.ObserveUserFoldersUseCase +import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCaseImpl 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 @@ -361,4 +363,6 @@ class ConversationScope internal constructor( get() = AddConversationToFavoritesUseCaseImpl(conversationFolderRepository) val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase get() = RemoveConversationFromFavoritesUseCaseImpl(conversationFolderRepository) + val observeUserFolders: ObserveUserFoldersUseCase + get() = ObserveUserFoldersUseCaseImpl(conversationFolderRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt index 4f7f9529f61..5a3b59405e8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt @@ -45,16 +45,26 @@ internal class ObserveConversationListDetailsWithEventsUseCaseImpl( fromArchive: Boolean, conversationFilter: ConversationFilter ): Flow> { - return if (conversationFilter == ConversationFilter.FAVORITES) { - when (val result = getFavoriteFolder()) { - GetFavoriteFolderUseCase.Result.Failure -> { - flowOf(emptyList()) + return when (conversationFilter) { + ConversationFilter.Favorites -> { + when (val result = getFavoriteFolder()) { + GetFavoriteFolderUseCase.Result.Failure -> { + flowOf(emptyList()) + } + + is GetFavoriteFolderUseCase.Result.Success -> + conversationFolderRepository.observeConversationsFromFolder(result.folder.id) } + } - is GetFavoriteFolderUseCase.Result.Success -> conversationFolderRepository.observeConversationsFromFolder(result.folder.id) + is ConversationFilter.Folder -> { + conversationFolderRepository.observeConversationsFromFolder(conversationFilter.folderId) } - } else { - conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter) + + ConversationFilter.All, + ConversationFilter.Groups, + ConversationFilter.OneOnOne -> + conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter) } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt index b0ffa60fa93..549b951c5fc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt @@ -19,7 +19,10 @@ 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.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn /** * This use case will observe and return the list of conversations from given folder. @@ -31,9 +34,10 @@ fun interface ObserveConversationsFromFolderUseCase { internal class ObserveConversationsFromFolderUseCaseImpl( private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : ObserveConversationsFromFolderUseCase { - override suspend operator fun invoke(folderId: String): Flow> { - return conversationFolderRepository.observeConversationsFromFolder(folderId) - } + override suspend operator fun invoke(folderId: String): Flow> = + conversationFolderRepository.observeConversationsFromFolder(folderId) + .flowOn(dispatchers.io) } 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 new file mode 100644 index 00000000000..3935cd2af28 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveUserFoldersUseCase.kt @@ -0,0 +1,46 @@ +/* + * 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.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.functional.mapToRightOr +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +/** + * This use case will observe and return the list of all user folders. + * @see ConversationFolder + */ +fun interface ObserveUserFoldersUseCase { + suspend operator fun invoke(): Flow> +} + +internal class ObserveUserFoldersUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : ObserveUserFoldersUseCase { + + override suspend operator fun invoke(): Flow> { + return conversationFolderRepository.observeUserFolders() + .mapToRightOr(emptyList()) + .flowOn(dispatchers.io) + } +} 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 db44a482e77..38d2c6db4bf 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,10 @@ CREATE TABLE LabeledConversation ( PRIMARY KEY (folder_id, conversation_id) ); +getUserFolders: +SELECT * FROM ConversationFolder +WHERE folder_type != 'FAVORITE'; + getAllFoldersWithConversations: SELECT conversationFolder.id AS label_id, @@ -60,8 +64,8 @@ VALUES(?, ?); deleteLabeledConversation: DELETE FROM LabeledConversation WHERE conversation_id = ? AND folder_id = ?; -clearFolders: -DELETE FROM ConversationFolder; - clearLabeledConversations: DELETE FROM LabeledConversation; + +clearFolders: +DELETE FROM ConversationFolder; 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 ed1ae82a9f8..0c201de5b8d 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 @@ -24,8 +24,9 @@ import kotlinx.coroutines.flow.Flow interface ConversationFolderDAO { suspend fun getFoldersWithConversations(): List suspend fun observeConversationListFromFolder(folderId: String): Flow> - suspend fun getFavoriteConversationFolder(): ConversationFolderEntity + suspend fun getFavoriteConversationFolder(): ConversationFolderEntity? suspend fun updateConversationFolders(folderWithConversationsList: List) suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) + suspend fun observeUserFolders(): 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 3316f030bd9..6e703b82419 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 @@ -18,6 +18,7 @@ package com.wire.kalium.persistence.dao.conversation.folder import app.cash.sqldelight.coroutines.asFlow +import com.wire.kalium.persistence.ConversationFolder import com.wire.kalium.persistence.ConversationFoldersQueries import com.wire.kalium.persistence.GetAllFoldersWithConversations import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -26,6 +27,7 @@ import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEvent import com.wire.kalium.persistence.util.mapToList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext @@ -35,6 +37,14 @@ class ConversationFolderDAOImpl internal constructor( ) : ConversationFolderDAO { private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper + override suspend fun observeUserFolders(): Flow> { + return conversationFoldersQueries.getUserFolders() + .asFlow() + .mapToList() + .map { it.map(::toEntity) } + .flowOn(coroutineContext) + } + override suspend fun getFoldersWithConversations(): List = withContext(coroutineContext) { val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().executeAsList().map(::toEntity) @@ -59,6 +69,12 @@ class ConversationFolderDAOImpl internal constructor( conversationId = row.conversation_id ) + private fun toEntity(row: ConversationFolder) = ConversationFolderEntity( + id = row.id, + name = row.name, + type = row.folder_type + ) + override suspend fun observeConversationListFromFolder(folderId: String): Flow> { return conversationFoldersQueries.getConversationsFromFolder( folderId, @@ -69,11 +85,11 @@ class ConversationFolderDAOImpl internal constructor( .flowOn(coroutineContext) } - override suspend fun getFavoriteConversationFolder(): ConversationFolderEntity { + override suspend fun getFavoriteConversationFolder(): ConversationFolderEntity? { return conversationFoldersQueries.getFavoriteFolder { id, name, folderType -> ConversationFolderEntity(id, name, folderType) } - .executeAsOne() + .executeAsOneOrNull() } override suspend fun updateConversationFolders(folderWithConversationsList: List) = 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 c55255dd733..d533ccc6dec 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 @@ -59,7 +59,8 @@ class ConversationFolderDAOTest : BaseDatabaseTest() { id = folderId, name = "folderName", type = ConversationFolderTypeEntity.USER, - conversationIdList = listOf(conversationEntity1.id)) + conversationIdList = listOf(conversationEntity1.id) + ) db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity)) val result = db.conversationFolderDAO.observeConversationListFromFolder(folderId).first().first() @@ -79,12 +80,13 @@ class ConversationFolderDAOTest : BaseDatabaseTest() { id = folderId, name = "", type = ConversationFolderTypeEntity.FAVORITE, - conversationIdList = listOf(conversationEntity1.id)) + conversationIdList = listOf(conversationEntity1.id) + ) db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity)) val result = db.conversationFolderDAO.getFavoriteConversationFolder() - assertEquals(folderId, result.id) + assertEquals(folderId, result?.id) } @Test