From d140d791a308632e568b0ee3f8685644bad90424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Fri, 15 Nov 2024 13:11:51 +0100 Subject: [PATCH] feat: conversation folders [WPB-14309] (#3106) * feat: conversation folders * feat: conversation folders * fix: labels converersion * detekt fix * fix: handle favorite conversations * remove unnecessary prints * detekt fix * review fixes * detekt fix * tests fix * test fix * renamed tests --- .../data/conversation/ConversationFolder.kt | 38 +++++ .../com/wire/kalium/logger/KaliumLogger.kt | 2 +- .../data/conversation/ConversationMapper.kt | 1 + .../conversation/ConversationRepository.kt | 31 +++- .../ConversationRepositoryExtensions.kt | 34 +--- .../folders/ConversationFolderMappers.kt | 65 +++++++ .../folders/ConversationFolderRepository.kt | 100 +++++++++++ .../com/wire/kalium/logic/data/event/Event.kt | 12 ++ .../kalium/logic/data/event/EventMapper.kt | 12 +- .../data/properties/UserPropertyRepository.kt | 11 ++ .../kalium/logic/data/sync/SlowSyncStatus.kt | 1 + .../kalium/logic/feature/UserSessionScope.kt | 23 ++- .../feature/conversation/ConversationScope.kt | 13 +- ...rsationListDetailsWithEventsUseCaseImpl.kt | 25 ++- .../folder/GetFavoriteFolderUseCase.kt | 49 ++++++ .../ObserveConversationsFromFolderUseCase.kt | 39 +++++ .../folder/SyncConversationFoldersUseCase.kt | 36 ++++ .../receiver/UserPropertiesEventReceiver.kt | 18 +- .../kalium/logic/sync/slow/SlowSyncManager.kt | 2 +- .../kalium/logic/sync/slow/SlowSyncWorker.kt | 3 + .../ConversationRepositoryExtensionsTest.kt | 2 +- .../ConversationRepositoryTest.kt | 10 +- .../ConversationFolderRepositoryTest.kt | 160 ++++++++++++++++++ .../properties/UserPropertyRepositoryTest.kt | 5 +- .../wire/kalium/logic/framework/TestEvent.kt | 16 ++ .../UserPropertiesEventReceiverTest.kt | 29 +++- .../logic/sync/slow/SlowSyncWorkerTest.kt | 13 ++ .../notification/EventContentDTO.kt | 46 +++-- .../notification/EventSerialization.kt | 4 +- .../api/authenticated/properties/LabelDTO.kt | 61 +++++++ .../authenticated/properties/PropertyKey.kt | 3 +- .../authenticated/properties/PropertiesApi.kt | 2 + .../api/v0/authenticated/PropertiesApiV0.kt | 8 +- .../kalium/persistence/ConversationDetails.sq | 10 ++ .../kalium/persistence/ConversationFolders.sq | 48 ++++++ .../src/commonMain/db_user/migrations/91.sqm | 19 +++ .../dao/conversation/ConversationDAO.kt | 2 +- .../dao/conversation/ConversationDAOImpl.kt | 7 +- .../folder/ConversationFolderDAO.kt | 27 +++ .../folder/ConversationFolderDAOImpl.kt | 73 ++++++++ .../folder/ConversationFolderEntity.kt | 38 +++++ .../wire/kalium/persistence/db/TableMapper.kt | 10 ++ .../persistence/db/UserDatabaseBuilder.kt | 10 ++ .../persistence/dao/ConnectionDaoTest.kt | 2 - .../persistence/dao/ConversationDAOTest.kt | 34 ++-- .../folder/ConversationFolderDAOTest.kt | 102 +++++++++++ 46 files changed, 1166 insertions(+), 90 deletions(-) create mode 100644 data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCase.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/SyncConversationFoldersUseCase.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt create mode 100644 network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/LabelDTO.kt create mode 100644 persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq create mode 100644 persistence/src/commonMain/db_user/migrations/91.sqm create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt create mode 100644 persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt 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 new file mode 100644 index 00000000000..675e9f5794f --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt @@ -0,0 +1,38 @@ +/* + * 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.data.conversation + +import com.wire.kalium.logic.data.id.QualifiedID + +data class ConversationFolder( + val id: String, + val name: String, + val type: FolderType +) + +data class FolderWithConversations( + val id: String, + val name: String, + val type: FolderType, + val conversationIdList: List +) + +enum class FolderType { + USER, + FAVORITE, +} diff --git a/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt b/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt index 8e77dfb90ac..f029a6bf0df 100644 --- a/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt +++ b/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt @@ -259,7 +259,7 @@ class KaliumLogger( enum class ApplicationFlow { SYNC, EVENT_RECEIVER, CONVERSATIONS, CONNECTIONS, MESSAGES, SEARCH, SESSION, REGISTER, - CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS + CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS, CONVERSATIONS_FOLDERS } } } 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 7faad124799..7a37f8ca9e2 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 @@ -336,6 +336,7 @@ internal class ConversationMapperImpl( daoModel.lastMessage != null -> messageMapper.fromEntityToMessagePreview(daoModel.lastMessage!!) else -> null }, + hasNewActivitiesToShow = daoModel.hasNewActivitiesToShow ) override fun fromDaoModel(daoModel: ProposalTimerEntity): ProposalTimer = 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 f5283b9d389..f199fe862f7 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 @@ -37,7 +37,6 @@ import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toCrypto import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel -import com.wire.kalium.logic.data.message.MessageMapper import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.di.MapperProvider @@ -131,8 +130,16 @@ interface ConversationRepository { suspend fun getConversationList(): Either>> suspend fun observeConversationList(): Flow> - suspend fun observeConversationListDetails(fromArchive: Boolean): Flow> - suspend fun observeConversationListDetailsWithEvents(fromArchive: Boolean = false): Flow> + suspend fun observeConversationListDetails( + fromArchive: Boolean, + conversationFilter: ConversationFilter = ConversationFilter.ALL + ): Flow> + + suspend fun observeConversationListDetailsWithEvents( + fromArchive: Boolean = false, + conversationFilter: ConversationFilter = ConversationFilter.ALL + ): Flow> + suspend fun getConversationIds( type: Conversation.Type, protocol: Conversation.Protocol, @@ -328,11 +335,10 @@ internal class ConversationDataSource internal constructor( private val conversationStatusMapper: ConversationStatusMapper = MapperProvider.conversationStatusMapper(), private val conversationRoleMapper: ConversationRoleMapper = MapperProvider.conversationRoleMapper(), private val protocolInfoMapper: ProtocolInfoMapper = MapperProvider.protocolInfoMapper(), - private val messageMapper: MessageMapper = MapperProvider.messageMapper(selfUserId), private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper() ) : ConversationRepository { override val extensions: ConversationRepositoryExtensions = - ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper, messageMapper) + ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper) // region Get/Observe by id @@ -353,6 +359,7 @@ internal class ConversationDataSource internal constructor( conversationMapper.fromConversationEntityType(it) } } + override suspend fun observeConversationDetailsById(conversationID: ConversationId): Flow> = conversationDAO.observeConversationDetailsById(conversationID.toDao()) .wrapStorageRequest() @@ -517,14 +524,20 @@ internal class ConversationDataSource internal constructor( return conversationDAO.getAllConversations().map { it.map(conversationMapper::fromDaoModel) } } - override suspend fun observeConversationListDetails(fromArchive: Boolean): Flow> = - conversationDAO.getAllConversationDetails(fromArchive).map { conversationViewEntityList -> + override suspend fun observeConversationListDetails( + fromArchive: Boolean, + conversationFilter: ConversationFilter + ): Flow> = + conversationDAO.getAllConversationDetails(fromArchive, conversationFilter.toDao()).map { conversationViewEntityList -> conversationViewEntityList.map { conversationViewEntity -> conversationMapper.fromDaoModelToDetails(conversationViewEntity) } } - override suspend fun observeConversationListDetailsWithEvents(fromArchive: Boolean): Flow> = + override suspend fun observeConversationListDetailsWithEvents( + fromArchive: Boolean, + conversationFilter: ConversationFilter + ): Flow> = combine( - conversationDAO.getAllConversationDetails(fromArchive), + conversationDAO.getAllConversationDetails(fromArchive, conversationFilter.toDao()), if (fromArchive) flowOf(listOf()) else messageDAO.observeLastMessages(), messageDAO.observeConversationsUnreadEvents(), messageDraftDAO.observeMessageDrafts() 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 f5017a0d968..aec18932a97 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 @@ -20,13 +20,10 @@ package com.wire.kalium.logic.data.conversation import app.cash.paging.PagingConfig import app.cash.paging.PagingData import app.cash.paging.map -import com.wire.kalium.logic.data.message.MessageMapper -import com.wire.kalium.logic.data.message.UnreadEventType import com.wire.kalium.persistence.dao.conversation.ConversationDAO import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig import com.wire.kalium.persistence.dao.message.KaliumPager -import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -40,8 +37,7 @@ interface ConversationRepositoryExtensions { class ConversationRepositoryExtensionsImpl internal constructor( private val conversationDAO: ConversationDAO, - private val conversationMapper: ConversationMapper, - private val messageMapper: MessageMapper, + private val conversationMapper: ConversationMapper ) : ConversationRepositoryExtensions { override suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( queryConfig: ConversationQueryConfig, @@ -61,27 +57,13 @@ class ConversationRepositoryExtensionsImpl internal constructor( ) } - return pager.pagingDataFlow.map { - it.map { - ConversationDetailsWithEvents( - conversationDetails = conversationMapper.fromDaoModelToDetails(it.conversationViewEntity), - lastMessage = when { - it.messageDraft != null -> messageMapper.fromDraftToMessagePreview(it.messageDraft!!) - it.lastMessage != null -> messageMapper.fromEntityToMessagePreview(it.lastMessage!!) - else -> null - }, - unreadEventCount = it.unreadEvents.unreadEvents.mapKeys { - when (it.key) { - UnreadEventTypeEntity.KNOCK -> UnreadEventType.KNOCK - UnreadEventTypeEntity.MISSED_CALL -> UnreadEventType.MISSED_CALL - UnreadEventTypeEntity.MENTION -> UnreadEventType.MENTION - UnreadEventTypeEntity.REPLY -> UnreadEventType.REPLY - UnreadEventTypeEntity.MESSAGE -> UnreadEventType.MESSAGE - } - }, - hasNewActivitiesToShow = it.hasNewActivitiesToShow, - ) - } + return pager.pagingDataFlow.map { pagingData -> + pagingData + .map { conversationDetailsWithEventsEntity -> + conversationMapper.fromDaoModelToDetailsWithEvents( + conversationDetailsWithEventsEntity + ) + } } } } 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 new file mode 100644 index 00000000000..aece3287d55 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt @@ -0,0 +1,65 @@ +/* + * 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.data.conversation.folders + +import com.wire.kalium.logic.data.conversation.ConversationFolder +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.data.id.toModel +import com.wire.kalium.network.api.authenticated.properties.LabelDTO +import com.wire.kalium.network.api.authenticated.properties.LabelTypeDTO +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity +import com.wire.kalium.persistence.dao.conversation.folder.FolderWithConversationsEntity + +fun LabelDTO.toFolder(selfDomain: String) = FolderWithConversations( + conversationIdList = qualifiedConversations?.map { it.toModel() } ?: conversations.map { QualifiedID(it, selfDomain) }, + id = id, + name = name, + type = type.toFolderType() +) + +fun LabelTypeDTO.toFolderType() = when (this) { + LabelTypeDTO.USER -> FolderType.USER + LabelTypeDTO.FAVORITE -> FolderType.FAVORITE +} + +fun ConversationFolderEntity.toModel() = ConversationFolder( + id = id, + name = name, + type = type.toModel() +) + +fun FolderWithConversations.toDao() = FolderWithConversationsEntity( + id = id, + name = name, + type = type.toDao(), + conversationIdList = conversationIdList.map { it.toDao() } +) + +fun FolderType.toDao() = when (this) { + FolderType.USER -> ConversationFolderTypeEntity.USER + FolderType.FAVORITE -> ConversationFolderTypeEntity.FAVORITE +} + +fun ConversationFolderTypeEntity.toModel(): FolderType = when (this) { + ConversationFolderTypeEntity.USER -> FolderType.USER + ConversationFolderTypeEntity.FAVORITE -> FolderType.FAVORITE +} 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 new file mode 100644 index 00000000000..ea1028f5c3f --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt @@ -0,0 +1,100 @@ +/* + * 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.data.conversation.folders + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.CONVERSATIONS_FOLDERS +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationFolder +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.di.MapperProvider +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.functional.onFailure +import com.wire.kalium.logic.functional.onSuccess +import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.wrapApiRequest +import com.wire.kalium.logic.wrapStorageRequest +import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal interface ConversationFolderRepository { + + suspend fun getFavoriteConversationFolder(): Either + suspend fun observeConversationsFromFolder(folderId: String): Flow> + suspend fun updateConversationFolders(folderWithConversations: List): Either + suspend fun fetchConversationFolders(): Either +} + +internal class ConversationFolderDataSource internal constructor( + private val conversationFolderDAO: ConversationFolderDAO, + private val userPropertiesApi: PropertiesApi, + private val selfUserId: QualifiedID, + private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId) +) : ConversationFolderRepository { + + override suspend fun updateConversationFolders(folderWithConversations: List): Either = + wrapStorageRequest { + conversationFolderDAO.updateConversationFolders(folderWithConversations.map { it.toDao() }) + } + + override suspend fun getFavoriteConversationFolder(): Either = wrapStorageRequest { + conversationFolderDAO.getFavoriteConversationFolder().toModel() + } + + override suspend fun observeConversationsFromFolder(folderId: String): Flow> = + conversationFolderDAO.observeConversationListFromFolder(folderId).map { conversationDetailsWithEventsEntityList -> + conversationDetailsWithEventsEntityList.map { + conversationMapper.fromDaoModelToDetailsWithEvents(it) + } + } + + override suspend fun fetchConversationFolders(): Either = wrapApiRequest { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Fetching conversation folders") + userPropertiesApi.getLabels() + } + .onSuccess { labelsResponse -> + val folders = labelsResponse.labels.map { it.toFolder(selfUserId.domain) }.toMutableList() + val favoriteLabel = folders.firstOrNull { it.type == FolderType.FAVORITE } + + if (favoriteLabel == null) { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Favorite label not found, creating a new one") + folders.add( + FolderWithConversations( + id = uuid4().toString(), + name = "", // name will be handled by localization + type = FolderType.FAVORITE, + conversationIdList = emptyList() + ) + ) + } + conversationFolderDAO.updateConversationFolders(folders.map { it.toDao() }) + } + .onFailure { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).e("Error fetching conversation folders $it") + Either.Left(it) + } + .map { } + +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt index 8a580fd5514..251fb918bec 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt @@ -29,6 +29,7 @@ import com.wire.kalium.logic.data.conversation.Conversation.Member import com.wire.kalium.logic.data.conversation.Conversation.Protocol import com.wire.kalium.logic.data.conversation.Conversation.ReceiptMode import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode +import com.wire.kalium.logic.data.conversation.FolderWithConversations import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.featureConfig.AppLockModel import com.wire.kalium.logic.data.featureConfig.ClassifiedDomainsModel @@ -708,6 +709,17 @@ sealed class Event(open val id: String) { "value" to "$value" ) } + + data class FoldersUpdate( + override val id: String, + val folders: List, + ) : UserProperty(id) { + override fun toLogMap(): Map = mapOf( + typeKey to "User.UserProperty.FoldersUpdate", + idKey to id.obfuscateId(), + "folders" to folders.map { it.id.obfuscateId() } + ) + } } data class Unknown( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt index ba1fb539fd8..58e95922ffa 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt @@ -28,6 +28,7 @@ import com.wire.kalium.logic.data.conversation.ConversationRoleMapper import com.wire.kalium.logic.data.conversation.MemberMapper import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.conversation.ReceiptModeMapper +import com.wire.kalium.logic.data.conversation.folders.toFolder import com.wire.kalium.logic.data.conversation.toModel import com.wire.kalium.logic.data.event.Event.UserProperty.ReadReceiptModeSet import com.wire.kalium.logic.data.event.Event.UserProperty.TypingIndicatorModeSet @@ -223,8 +224,8 @@ class EventMapper( ): Event { val fieldKeyValue = eventContentDTO.value val key = eventContentDTO.key - return when { - fieldKeyValue is EventContentDTO.FieldKeyNumberValue -> { + return when (fieldKeyValue) { + is EventContentDTO.FieldKeyNumberValue -> { when (key) { WIRE_RECEIPT_MODE.key -> ReadReceiptModeSet( id, @@ -244,7 +245,12 @@ class EventMapper( } } - else -> unknown( + is EventContentDTO.FieldLabelListValue -> Event.UserProperty.FoldersUpdate( + id = id, + folders = fieldKeyValue.value.labels.map { it.toFolder(selfUserId.domain) } + ) + + is EventContentDTO.FieldUnknownValue -> unknown( id = id, eventContentDTO = eventContentDTO, cause = "Unknown value type for key: ${eventContentDTO.key} " diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt index 0f7700af816..ce30758bd3c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt @@ -20,9 +20,13 @@ package com.wire.kalium.logic.data.properties import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.FolderWithConversations +import com.wire.kalium.logic.data.conversation.folders.toFolder +import com.wire.kalium.logic.data.user.UserId 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.logic.functional.map import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi import com.wire.kalium.network.api.authenticated.properties.PropertyKey @@ -38,11 +42,13 @@ interface UserPropertyRepository { suspend fun observeTypingIndicatorStatus(): Flow> suspend fun setTypingIndicatorEnabled(): Either suspend fun removeTypingIndicatorProperty(): Either + suspend fun getConversationFolders(): Either> } internal class UserPropertyDataSource( private val propertiesApi: PropertiesApi, private val userConfigRepository: UserConfigRepository, + private val selfUserId: UserId ) : UserPropertyRepository { override suspend fun getReadReceiptsStatus(): Boolean = userConfigRepository.isReadReceiptsEnabled() @@ -82,4 +88,9 @@ internal class UserPropertyDataSource( }.flatMap { userConfigRepository.setTypingIndicatorStatus(false) } + + override suspend fun getConversationFolders(): Either> = wrapApiRequest { + propertiesApi.getLabels() + } + .map { it.labels.map { label -> label.toFolder(selfUserId.domain) } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt index 6dbeb1274cc..5bb50ed3cf4 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt @@ -44,4 +44,5 @@ enum class SlowSyncStep { JOINING_MLS_CONVERSATIONS, RESOLVE_ONE_ON_ONE_PROTOCOLS, LEGAL_HOLD, + CONVERSATION_FOLDERS, } 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 7351839496c..53cf5806b89 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 @@ -78,6 +78,8 @@ import com.wire.kalium.logic.data.conversation.ProposalTimer import com.wire.kalium.logic.data.conversation.SubconversationRepositoryImpl import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProvider import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProviderImpl +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderDataSource +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepositoryDataSource import com.wire.kalium.logic.data.e2ei.E2EIRepository @@ -206,6 +208,8 @@ import com.wire.kalium.logic.feature.conversation.RecoverMLSConversationsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCaseImpl import com.wire.kalium.logic.feature.conversation.TypingIndicatorSyncManager +import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCase +import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCaseImpl import com.wire.kalium.logic.feature.conversation.keyingmaterials.KeyingMaterialsManager import com.wire.kalium.logic.feature.conversation.keyingmaterials.KeyingMaterialsManagerImpl import com.wire.kalium.logic.feature.conversation.mls.MLSOneOnOneConversationResolver @@ -613,7 +617,8 @@ class UserSessionScope internal constructor( private val userPropertyRepository: UserPropertyRepository get() = UserPropertyDataSource( authenticatedNetworkContainer.propertiesApi, - userConfigRepository + userConfigRepository, + userId ) private val keyPackageLimitsProvider: KeyPackageLimitsProvider @@ -713,6 +718,13 @@ class UserSessionScope internal constructor( userStorage.database.conversationMetaDataDAO, ) + private val conversationFolderRepository: ConversationFolderRepository + get() = ConversationFolderDataSource( + userStorage.database.conversationFolderDAO, + authenticatedNetworkContainer.propertiesApi, + userId + ) + private val conversationGroupRepository: ConversationGroupRepository get() = ConversationGroupRepositoryImpl( mlsConversationRepository, @@ -954,6 +966,9 @@ class UserSessionScope internal constructor( systemMessageInserter ) + private val syncConversationFolders: SyncConversationFoldersUseCase + get() = SyncConversationFoldersUseCaseImpl(conversationFolderRepository) + private val syncConnections: SyncConnectionsUseCase get() = SyncConnectionsUseCaseImpl( connectionRepository = connectionRepository @@ -1072,7 +1087,8 @@ class UserSessionScope internal constructor( syncContacts, joinExistingMLSConversations, fetchLegalHoldForSelfUserFromRemoteUseCase, - oneOnOneResolver + oneOnOneResolver, + syncConversationFolders ) } @@ -1586,7 +1602,7 @@ class UserSessionScope internal constructor( ) private val userPropertiesEventReceiver: UserPropertiesEventReceiver - get() = UserPropertiesEventReceiverImpl(userConfigRepository) + get() = UserPropertiesEventReceiverImpl(userConfigRepository, conversationFolderRepository) private val federationEventReceiver: FederationEventReceiver get() = FederationEventReceiverImpl( @@ -1754,6 +1770,7 @@ class UserSessionScope internal constructor( conversationGroupRepository, connectionRepository, userRepository, + conversationFolderRepository, syncManager, mlsConversationRepository, clientIdProvider, 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 7ee88554ded..7ea0b3ac407 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 @@ -34,6 +34,7 @@ import com.wire.kalium.logic.data.conversation.TypingIndicatorOutgoingRepository import com.wire.kalium.logic.data.conversation.TypingIndicatorSenderHandler import com.wire.kalium.logic.data.conversation.TypingIndicatorSenderHandlerImpl import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProvider +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.SelfTeamIdProvider @@ -48,6 +49,10 @@ 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.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.guestroomlink.CanCreatePasswordProtectedLinksUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCaseImpl @@ -81,6 +86,7 @@ class ConversationScope internal constructor( private val conversationGroupRepository: ConversationGroupRepository, private val connectionRepository: ConnectionRepository, private val userRepository: UserRepository, + private val conversationFolderRepository: ConversationFolderRepository, private val syncManager: SyncManager, private val mlsConversationRepository: MLSConversationRepository, private val currentClientIdProvider: CurrentClientIdProvider, @@ -119,7 +125,7 @@ class ConversationScope internal constructor( get() = ObserveConversationListDetailsUseCaseImpl(conversationRepository) val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase - get() = ObserveConversationListDetailsWithEventsUseCaseImpl(conversationRepository) + get() = ObserveConversationListDetailsWithEventsUseCaseImpl(conversationRepository, conversationFolderRepository, getFavoriteFolder) val observeConversationMembers: ObserveConversationMembersUseCase get() = ObserveConversationMembersUseCaseImpl(conversationRepository, userRepository) @@ -343,4 +349,9 @@ class ConversationScope internal constructor( get() = ObserveConversationUnderLegalHoldNotifiedUseCaseImpl(conversationRepository) val syncConversationCode: SyncConversationCodeUseCase get() = SyncConversationCodeUseCase(conversationGroupRepository, serverConfigLinks) + val observeConversationsFromFolder: ObserveConversationsFromFolderUseCase + get() = ObserveConversationsFromFolderUseCaseImpl(conversationFolderRepository) + val getFavoriteFolder: GetFavoriteFolderUseCase + get() = GetFavoriteFolderUseCaseImpl(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 543ca5d213c..4f7f9529f61 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 @@ -20,22 +20,41 @@ package com.wire.kalium.logic.feature.conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationFilter import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf /** * This use case will observe and return the list of conversation details for the current user. * @see ConversationDetails */ fun interface ObserveConversationListDetailsWithEventsUseCase { - suspend operator fun invoke(fromArchive: Boolean): Flow> + suspend operator fun invoke(fromArchive: Boolean, conversationFilter: ConversationFilter): Flow> } internal class ObserveConversationListDetailsWithEventsUseCaseImpl( private val conversationRepository: ConversationRepository, + private val conversationFolderRepository: ConversationFolderRepository, + private val getFavoriteFolder: GetFavoriteFolderUseCase ) : ObserveConversationListDetailsWithEventsUseCase { - override suspend operator fun invoke(fromArchive: Boolean): Flow> { - return conversationRepository.observeConversationListDetailsWithEvents(fromArchive) + override suspend operator fun invoke( + fromArchive: Boolean, + conversationFilter: ConversationFilter + ): Flow> { + return if (conversationFilter == ConversationFilter.FAVORITES) { + when (val result = getFavoriteFolder()) { + GetFavoriteFolderUseCase.Result.Failure -> { + flowOf(emptyList()) + } + + is GetFavoriteFolderUseCase.Result.Success -> conversationFolderRepository.observeConversationsFromFolder(result.folder.id) + } + } else { + conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter) + } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCase.kt new file mode 100644 index 00000000000..c8e7b950850 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCase.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/. + */ +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.feature.conversation.folder.GetFavoriteFolderUseCase.Result +import com.wire.kalium.logic.functional.fold + +/** + * This use case will return the favorite folder. + * @return [Result.Success] with [ConversationFolder] in case of success, + * or [Result.Failure] if something went wrong - can't get data from local DB. + */ +fun interface GetFavoriteFolderUseCase { + suspend operator fun invoke(): Result + + sealed class Result { + data class Success(val folder: ConversationFolder) : Result() + data object Failure : Result() + } +} + +internal class GetFavoriteFolderUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, +) : GetFavoriteFolderUseCase { + + override suspend operator fun invoke(): Result { + return conversationFolderRepository.getFavoriteConversationFolder().fold( + { Result.Failure }, + { Result.Success(it) } + ) + } +} 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 new file mode 100644 index 00000000000..b0ffa60fa93 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt @@ -0,0 +1,39 @@ +/* + * 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 kotlinx.coroutines.flow.Flow + +/** + * This use case will observe and return the list of conversations from given folder. + * @see ConversationDetailsWithEvents + */ +fun interface ObserveConversationsFromFolderUseCase { + suspend operator fun invoke(folderId: String): Flow> +} + +internal class ObserveConversationsFromFolderUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, +) : ObserveConversationsFromFolderUseCase { + + override suspend operator fun invoke(folderId: String): Flow> { + return conversationFolderRepository.observeConversationsFromFolder(folderId) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/SyncConversationFoldersUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/SyncConversationFoldersUseCase.kt new file mode 100644 index 00000000000..216290437e3 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/SyncConversationFoldersUseCase.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.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.functional.Either + +internal interface SyncConversationFoldersUseCase { + suspend operator fun invoke(): Either +} + +/** + * This use case will sync against the backend the conversation folders of the current user. + */ +internal class SyncConversationFoldersUseCaseImpl( + private val conversationRepository: ConversationFolderRepository, +) : SyncConversationFoldersUseCase { + override suspend operator fun invoke(): Either = conversationRepository.fetchConversationFolders() +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiver.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiver.kt index 3967f34d8ed..31ff54e6e9b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiver.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiver.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.sync.receiver import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.event.EventDeliveryInfo import com.wire.kalium.logic.functional.Either @@ -31,7 +32,8 @@ import com.wire.kalium.logic.util.createEventProcessingLogger internal interface UserPropertiesEventReceiver : EventReceiver internal class UserPropertiesEventReceiverImpl internal constructor( - private val userConfigRepository: UserConfigRepository + private val userConfigRepository: UserConfigRepository, + private val conversationFolderRepository: ConversationFolderRepository ) : UserPropertiesEventReceiver { override suspend fun onEvent(event: Event.UserProperty, deliveryInfo: EventDeliveryInfo): Either { @@ -43,6 +45,10 @@ internal class UserPropertiesEventReceiverImpl internal constructor( is Event.UserProperty.TypingIndicatorModeSet -> { handleTypingIndicatorMode(event) } + + is Event.UserProperty.FoldersUpdate -> { + handleFoldersUpdate(event) + } } } @@ -65,4 +71,14 @@ internal class UserPropertiesEventReceiverImpl internal constructor( .onSuccess { logger.logSuccess() } .onFailure { logger.logFailure(it) } } + + private suspend fun handleFoldersUpdate( + event: Event.UserProperty.FoldersUpdate + ): Either { + val logger = kaliumLogger.createEventProcessingLogger(event) + return conversationFolderRepository + .updateConversationFolders(event.folders) + .onSuccess { logger.logSuccess() } + .onFailure { logger.logFailure(it) } + } } 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 f3c25780c6e..722ad9e3d81 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 = 7 + const val CURRENT_VERSION = 8 val MIN_RETRY_DELAY = 1.seconds val MAX_RETRY_DELAY = 10.minutes diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorker.kt index 35fc040ee08..2022b6c0f32 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorker.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorker.kt @@ -26,6 +26,7 @@ import com.wire.kalium.logic.data.event.EventRepository import com.wire.kalium.logic.data.sync.SlowSyncStep import com.wire.kalium.logic.feature.connection.SyncConnectionsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCase +import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCase import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver import com.wire.kalium.logic.feature.featureConfig.SyncFeatureConfigsUseCase import com.wire.kalium.logic.feature.legalhold.FetchLegalHoldForSelfUserFromRemoteUseCase @@ -71,6 +72,7 @@ internal class SlowSyncWorkerImpl( private val joinMLSConversations: JoinExistingMLSConversationsUseCase, private val fetchLegalHoldForSelfUserFromRemoteUseCase: FetchLegalHoldForSelfUserFromRemoteUseCase, private val oneOnOneResolver: OneOnOneResolver, + private val syncConversationFolders: SyncConversationFoldersUseCase, logger: KaliumLogger = kaliumLogger ) : SlowSyncWorker { @@ -102,6 +104,7 @@ internal class SlowSyncWorkerImpl( .continueWithStep(SlowSyncStep.CONTACTS, syncContacts::invoke) .continueWithStep(SlowSyncStep.JOINING_MLS_CONVERSATIONS, joinMLSConversations::invoke) .continueWithStep(SlowSyncStep.RESOLVE_ONE_ON_ONE_PROTOCOLS, oneOnOneResolver::resolveAllOneOnOneConversations) + .continueWithStep(SlowSyncStep.CONVERSATION_FOLDERS, syncConversationFolders::invoke) .flatMap { saveLastProcessedEventIdIfNeeded(lastProcessedEventIdToSaveOnSuccess) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt index 82ee1f9d0aa..dbc66271f5d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt @@ -93,7 +93,7 @@ class ConversationRepositoryExtensionsTest { @Mock private val messageMapper: MessageMapper = mock(MessageMapper::class) private val conversationRepositoryExtensions: ConversationRepositoryExtensions by lazy { - ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper, messageMapper) + ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper) } init { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt index 70f0807d7d6..c7108c8b7b7 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt @@ -48,10 +48,8 @@ import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangement import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangementImpl import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed -import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol.MLS -import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO import com.wire.kalium.network.api.authenticated.conversation.ConversationMembersResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationNameUpdateEvent @@ -69,6 +67,8 @@ import com.wire.kalium.network.api.authenticated.conversation.model.Conversation import com.wire.kalium.network.api.authenticated.conversation.model.ConversationProtocolDTO import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO import com.wire.kalium.network.api.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.base.authenticated.client.ClientApi +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.network.api.model.ConversationAccessDTO import com.wire.kalium.network.api.model.ConversationAccessRoleDTO import com.wire.kalium.network.exceptions.KaliumException @@ -80,8 +80,8 @@ import com.wire.kalium.persistence.dao.client.ClientDAO import com.wire.kalium.persistence.dao.client.ClientTypeEntity import com.wire.kalium.persistence.dao.client.DeviceTypeEntity import com.wire.kalium.persistence.dao.conversation.ConversationDAO -import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity import com.wire.kalium.persistence.dao.message.MessageDAO @@ -100,7 +100,6 @@ import io.mockative.any import io.mockative.coEvery import io.mockative.coVerify import io.mockative.eq -import io.mockative.every import io.mockative.fake.valueOf import io.mockative.matchers.AnyMatcher import io.mockative.matchers.EqualsMatcher @@ -120,7 +119,6 @@ import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull -import kotlin.test.assertTrue import com.wire.kalium.network.api.model.ConversationId as APIConversationId import com.wire.kalium.persistence.dao.client.Client as ClientEntity @@ -1516,7 +1514,7 @@ class ConversationRepositoryTest { suspend fun withConversations(conversations: List) = apply { coEvery { - conversationDAO.getAllConversationDetails(any()) + conversationDAO.getAllConversationDetails(any(), any()) }.returns(flowOf(conversations)) } 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 new file mode 100644 index 00000000000..88557c7bc48 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt @@ -0,0 +1,160 @@ +/* + * 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.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.di.MapperProvider +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.util.shouldFail +import com.wire.kalium.logic.util.shouldSucceed +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO +import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi +import com.wire.kalium.network.exceptions.KaliumException +import com.wire.kalium.network.utils.NetworkResponse +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity +import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity +import io.ktor.util.reflect.instanceOf +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConversationFolderRepositoryTest { + + @Test + fun givenFavoriteFolderExistsWhenFetchingFavoriteFolderThenShouldReturnFolderSuccessfully() = runTest { + // given + val folder = ConversationFolderEntity(id = "folder1", name = "Favorites", type = ConversationFolderTypeEntity.FAVORITE) + val arrangement = Arrangement().withFavoriteConversationFolder(folder) + + // when + val result = arrangement.repository.getFavoriteConversationFolder() + + // then + result.shouldSucceed { + assertEquals(folder.toModel(), it) + } + coVerify { arrangement.conversationFolderDAO.getFavoriteConversationFolder() }.wasInvoked() + } + + @Test + fun givenConversationsInFolderWhenObservingConversationsFromFolderThenShouldEmitConversationsList() = runTest { + // given + val folderId = "folder1" + val conversation = ConversationDetailsWithEventsEntity( + conversationViewEntity = TestConversation.VIEW_ENTITY, + lastMessage = null, + messageDraft = null, + unreadEvents = ConversationUnreadEventEntity(TestConversation.VIEW_ENTITY.id, mapOf()), + ) + + val conversations = listOf(conversation) + val arrangement = Arrangement().withConversationsFromFolder(folderId, conversations) + + // when + val resultFlow = arrangement.repository.observeConversationsFromFolder(folderId) + + // then + val emittedConversations = resultFlow.first() + assertEquals(arrangement.conversationMapper.fromDaoModelToDetailsWithEvents(conversations.first()), emittedConversations.first()) + } + + @Test + fun givenFolderDataWhenUpdatingConversationFoldersThenFoldersShouldBeUpdatedInDatabaseSuccessfully() = runTest { + // given + val folders = listOf( + FolderWithConversations( + id = "folder1", name = "Favorites", type = FolderType.FAVORITE, + conversationIdList = listOf() + ) + ) + val arrangement = Arrangement().withSuccessfulFolderUpdate() + + // when + val result = arrangement.repository.updateConversationFolders(folders) + + // then + result.shouldSucceed() + coVerify { arrangement.conversationFolderDAO.updateConversationFolders(any()) }.wasInvoked() + } + + @Test + fun givenNetworkFailureWhenFetchingConversationFoldersThenShouldReturnNetworkFailure() = runTest { + // given + val arrangement = Arrangement().withFetchConversationLabels(NetworkResponse.Error(KaliumException.NoNetwork())) + + // when + val result = arrangement.repository.fetchConversationFolders() + + // then + result.shouldFail { failure -> + failure.instanceOf(NetworkFailure.NoNetworkConnection::class) + } + } + + private class Arrangement { + + @Mock + val conversationFolderDAO = mock(ConversationFolderDAO::class) + + @Mock + val userPropertiesApi = mock(PropertiesApi::class) + + private val selfUserId = TestUser.SELF.id + + val conversationMapper = MapperProvider.conversationMapper(selfUserId) + + val repository = ConversationFolderDataSource( + conversationFolderDAO = conversationFolderDAO, + userPropertiesApi = userPropertiesApi, + selfUserId = selfUserId + ) + + suspend fun withFavoriteConversationFolder(folder: ConversationFolderEntity): Arrangement { + coEvery { conversationFolderDAO.getFavoriteConversationFolder() }.returns(folder) + return this + } + + suspend fun withConversationsFromFolder(folderId: String, conversations: List): Arrangement { + coEvery { conversationFolderDAO.observeConversationListFromFolder(folderId) }.returns(flowOf(conversations)) + return this + } + + suspend fun withSuccessfulFolderUpdate(): Arrangement { + coEvery { conversationFolderDAO.updateConversationFolders(any()) }.returns(Unit) + return this + } + + suspend fun withFetchConversationLabels(response: NetworkResponse): Arrangement { + coEvery { userPropertiesApi.getLabels() }.returns(response) + return this + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepositoryTest.kt index b4890d9af59..f47c7bff692 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepositoryTest.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.properties import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.util.shouldSucceed import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi @@ -101,7 +102,9 @@ class UserPropertyRepositoryTest { @Mock val userConfigRepository = mock(UserConfigRepository::class) - private val userPropertyRepository = UserPropertyDataSource(propertiesApi, userConfigRepository) + private val selfUserId = TestUser.SELF.id + + private val userPropertyRepository = UserPropertyDataSource(propertiesApi, userConfigRepository, selfUserId) suspend fun withUpdateReadReceiptsSuccess() = apply { coEvery { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt index 4c3e4f1cec6..5536aa2bcae 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt @@ -22,6 +22,8 @@ import com.wire.kalium.cryptography.utils.EncryptedData import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.Conversation.Member +import com.wire.kalium.logic.data.conversation.FolderType +import com.wire.kalium.logic.data.conversation.FolderWithConversations import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.event.EventDeliveryInfo @@ -33,6 +35,8 @@ import com.wire.kalium.logic.data.user.Connection import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.sync.incremental.EventSource +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity import com.wire.kalium.util.time.UNIX_FIRST_DATE import io.ktor.util.encodeBase64 import kotlinx.datetime.Instant @@ -167,6 +171,18 @@ object TestEvent { value = true ) + fun foldersUpdate(eventId: String = "eventId") = Event.UserProperty.FoldersUpdate( + id = eventId, + folders = listOf( + FolderWithConversations( + id = "folder1", + name = "Favorites", + type = FolderType.FAVORITE, + conversationIdList = listOf(TestConversation.ID) + ) + ) + ) + fun newMessageEvent( encryptedContent: String, senderUserId: UserId = TestUser.USER_ID, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiverTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiverTest.kt index 6ba11ddfe57..af93f739d10 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiverTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiverTest.kt @@ -19,10 +19,13 @@ package com.wire.kalium.logic.sync.receiver import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.framework.TestEvent 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.every import io.mockative.mock import io.mockative.once @@ -46,13 +49,31 @@ class UserPropertiesEventReceiverTest { }.wasInvoked(exactly = once) } + @Test + fun givenFoldersUpdateEvent_repositoryIsInvoked() = runTest { + val event = TestEvent.foldersUpdate() + val (arrangement, eventReceiver) = Arrangement() + .withUpdateConversationFolders() + .arrange() + + eventReceiver.onEvent(event, TestEvent.liveDeliveryInfo) + + coVerify { + arrangement.conversationFolderRepository.updateConversationFolders(any()) + }.wasInvoked(exactly = once) + } + private class Arrangement { @Mock val userConfigRepository = mock(UserConfigRepository::class) + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + private val userPropertiesEventReceiver: UserPropertiesEventReceiver = UserPropertiesEventReceiverImpl( - userConfigRepository = userConfigRepository + userConfigRepository = userConfigRepository, + conversationFolderRepository = conversationFolderRepository ) fun withUpdateReadReceiptsSuccess() = apply { @@ -61,6 +82,12 @@ class UserPropertiesEventReceiverTest { }.returns(Either.Right(Unit)) } + suspend fun withUpdateConversationFolders() = apply { + coEvery { + conversationFolderRepository.updateConversationFolders(any()) + }.returns(Either.Right(Unit)) + } + fun arrange() = this to userPropertiesEventReceiver } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorkerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorkerTest.kt index f9dbe4f7ee5..facf5ddb70b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorkerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorkerTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.sync.SlowSyncStep import com.wire.kalium.logic.data.user.LegalHoldStatus import com.wire.kalium.logic.feature.connection.SyncConnectionsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCase +import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCase import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver import com.wire.kalium.logic.feature.featureConfig.SyncFeatureConfigsUseCase import com.wire.kalium.logic.feature.legalhold.FetchLegalHoldForSelfUserFromRemoteUseCase @@ -71,6 +72,7 @@ class SlowSyncWorkerTest { .withJoinMLSConversationsSuccess() .withResolveOneOnOneConversationsSuccess() .withFetchLegalHoldStatusSuccess() + .withSyncFoldersSuccess() .arrange() worker.slowSyncStepsFlow(successfullyMigration).collect() @@ -408,6 +410,7 @@ class SlowSyncWorkerTest { .withJoinMLSConversationsSuccess() .withResolveOneOnOneConversationsSuccess() .withFetchLegalHoldStatusSuccess() + .withSyncFoldersSuccess() .arrange() slowSyncWorker.slowSyncStepsFlow(successfullyMigration).collect() @@ -511,6 +514,9 @@ class SlowSyncWorkerTest { @Mock val fetchLegalHoldForSelfUserFromRemoteUseCase = mock(FetchLegalHoldForSelfUserFromRemoteUseCase::class) + @Mock + val syncConversationFoldersUseCase = mock(SyncConversationFoldersUseCase::class) + init { runBlocking { withLastProcessedEventIdReturning(Either.Right("lastProcessedEventId")) @@ -529,6 +535,7 @@ class SlowSyncWorkerTest { updateSupportedProtocols = updateSupportedProtocols, fetchLegalHoldForSelfUserFromRemoteUseCase = fetchLegalHoldForSelfUserFromRemoteUseCase, oneOnOneResolver = oneOnOneResolver, + syncConversationFolders = syncConversationFoldersUseCase ) suspend fun withSyncSelfUserFailure() = apply { @@ -644,6 +651,12 @@ class SlowSyncWorkerTest { oneOnOneResolver.resolveAllOneOnOneConversations(any()) }.returns(success) } + + suspend fun withSyncFoldersSuccess() = apply { + coEvery { + syncConversationFoldersUseCase.invoke() + }.returns(success) + } } private companion object { diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventContentDTO.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventContentDTO.kt index 52ce822a18e..1f29d78dead 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventContentDTO.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventContentDTO.kt @@ -39,6 +39,7 @@ import com.wire.kalium.network.api.authenticated.notification.conversation.Messa import com.wire.kalium.network.api.authenticated.notification.team.TeamMemberIdData import com.wire.kalium.network.api.authenticated.notification.user.RemoveClientEventData import com.wire.kalium.network.api.authenticated.notification.user.UserUpdateEventData +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO import com.wire.kalium.network.api.model.ConversationId import com.wire.kalium.network.api.model.TeamId import com.wire.kalium.network.api.model.UserId @@ -51,7 +52,6 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor @@ -62,9 +62,12 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.decodeStructure import kotlinx.serialization.encoding.encodeStructure +import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonTransformingSerializer +import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonObject import kotlin.jvm.JvmInline @@ -391,7 +394,6 @@ sealed class EventContentDTO { data class PropertiesDeleteDTO( @SerialName("key") val key: String, ) : UserProperty() - } @Serializable(with = FieldKeyValueDeserializer::class) @@ -405,33 +407,57 @@ sealed class EventContentDTO { @JvmInline value class FieldUnknownValue(val value: String) : FieldKeyValue + @Serializable + @JvmInline + value class FieldLabelListValue(val value: LabelListResponseDTO) : FieldKeyValue + @Serializable @SerialName("unknown") - data class Unknown( - val type: String - ) : EventContentDTO() + data class Unknown(val type: String) : EventContentDTO() } @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) -@Serializer(EventContentDTO.FieldKeyValue::class) object FieldKeyValueDeserializer : KSerializer { override val descriptor = buildSerialDescriptor("value", PolymorphicKind.SEALED) override fun serialize(encoder: Encoder, value: EventContentDTO.FieldKeyValue) { when (value) { is EventContentDTO.FieldKeyNumberValue -> encoder.encodeInt(value.value) + is EventContentDTO.FieldLabelListValue -> encoder.encodeSerializableValue( + EventContentDTO.FieldLabelListValue.serializer(), + value + ) + is EventContentDTO.FieldUnknownValue -> throw SerializationException("Not handled yet") } } - @Suppress("TooGenericExceptionCaught") + @Suppress("TooGenericExceptionCaught", "ReturnCount") override fun deserialize(decoder: Decoder): EventContentDTO.FieldKeyValue { - return try { - EventContentDTO.FieldKeyNumberValue(decoder.decodeInt()) + try { + val input = decoder as? JsonDecoder ?: throw SerializationException("Expected JsonDecoder") + return when (val element = input.decodeJsonElement()) { + is JsonPrimitive -> { + if (element.isString) { + EventContentDTO.FieldUnknownValue(element.content) + } else { + EventContentDTO.FieldKeyNumberValue(element.int) + } + } + + is JsonObject -> { + if (element.containsKey("labels")) { + return input.json.decodeFromJsonElement(EventContentDTO.FieldLabelListValue.serializer(), element) + } + EventContentDTO.FieldUnknownValue(element.toString()) + } + + else -> throw SerializationException("Unexpected JSON element type: ${element::class.simpleName}") + } } catch (exception: Exception) { val jsonElement = decoder.toJsonElement().toString() kaliumUtilLogger.d("Error deserializing 'user.properties-set', prop: $jsonElement") kaliumUtilLogger.w("Error deserializing 'user.properties-set', error: $exception") - EventContentDTO.FieldUnknownValue(jsonElement) + return EventContentDTO.FieldUnknownValue(jsonElement) } } } diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventSerialization.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventSerialization.kt index 45c3b2c7f03..819b964aed3 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventSerialization.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventSerialization.kt @@ -25,9 +25,9 @@ import kotlinx.serialization.modules.polymorphic internal val eventSerializationModule = SerializersModule { polymorphic(EventContentDTO::class) { polymorphic(FeatureConfigData::class) { - default { FeatureConfigData.Unknown.serializer() } + defaultDeserializer { FeatureConfigData.Unknown.serializer() } } - default { EventContentDTO.Unknown.serializer() } + defaultDeserializer { EventContentDTO.Unknown.serializer() } } } diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/LabelDTO.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/LabelDTO.kt new file mode 100644 index 00000000000..b5d15a9782f --- /dev/null +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/LabelDTO.kt @@ -0,0 +1,61 @@ +/* + * 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.network.api.authenticated.properties + +import com.wire.kalium.network.api.model.QualifiedID +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable +data class LabelListResponseDTO( + @SerialName("labels") val labels: List +) + +@Serializable +data class LabelDTO( + @SerialName("id") val id: String, + @SerialName("name") val name: String, + @Serializable(with = LabelTypeSerializer::class) + @SerialName("type") val type: LabelTypeDTO, + @Deprecated("Use qualifiedConversations instead") + @SerialName("conversations") val conversations: List, + @SerialName("qualified_conversations") val qualifiedConversations: List? +) + +enum class LabelTypeDTO { + USER, + FAVORITE +} + +object LabelTypeSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("type", PrimitiveKind.INT) + + override fun serialize(encoder: Encoder, value: LabelTypeDTO) { + encoder.encodeInt(value.ordinal) + } + + override fun deserialize(decoder: Decoder): LabelTypeDTO { + val ordinal = decoder.decodeInt() + return LabelTypeDTO.entries.getOrElse(ordinal) { LabelTypeDTO.USER } + } +} diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/PropertyKey.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/PropertyKey.kt index 7e20e0b97e3..c532b1a8aad 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/PropertyKey.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/PropertyKey.kt @@ -19,6 +19,7 @@ package com.wire.kalium.network.api.authenticated.properties enum class PropertyKey(val key: String) { WIRE_RECEIPT_MODE("WIRE_RECEIPT_MODE"), - WIRE_TYPING_INDICATOR_MODE("WIRE_TYPING_INDICATOR_MODE") + WIRE_TYPING_INDICATOR_MODE("WIRE_TYPING_INDICATOR_MODE"), + WIRE_LABELS("labels"), // TODO map other event like -ie. 'labels'- } 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 c4760248b55..c224ec6b133 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 @@ -18,6 +18,7 @@ package com.wire.kalium.network.api.base.authenticated.properties +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO import com.wire.kalium.network.api.authenticated.properties.PropertyKey import com.wire.kalium.network.utils.NetworkResponse @@ -25,5 +26,6 @@ interface PropertiesApi { suspend fun setProperty(propertyKey: PropertyKey, propertyValue: Any): NetworkResponse suspend fun deleteProperty(propertyKey: PropertyKey): NetworkResponse + suspend fun getLabels(): 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 4672a33e35a..7a556a9cf08 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 @@ -19,11 +19,13 @@ package com.wire.kalium.network.api.v0.authenticated import com.wire.kalium.network.AuthenticatedNetworkClient -import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO import com.wire.kalium.network.api.authenticated.properties.PropertyKey +import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.network.utils.wrapKaliumResponse import io.ktor.client.request.delete +import io.ktor.client.request.get import io.ktor.client.request.put import io.ktor.client.request.setBody @@ -35,6 +37,7 @@ internal open class PropertiesApiV0 internal constructor( private companion object { const val PATH_PROPERTIES = "properties" + const val PATH_LABELS = "labels" } override suspend fun setProperty(propertyKey: PropertyKey, propertyValue: Any): NetworkResponse = @@ -46,4 +49,7 @@ internal open class PropertiesApiV0 internal constructor( httpClient.delete("$PATH_PROPERTIES/${propertyKey.key}") } + override suspend fun getLabels(): NetworkResponse = wrapKaliumResponse { + httpClient.get("$PATH_PROPERTIES/$PATH_LABELS") + } } 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 88746b79ac5..8cab007519e 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 @@ -138,6 +138,16 @@ WHERE AND (protocol IS 'PROTEUS' OR protocol IS 'MIXED' OR (protocol IS 'MLS' AND mls_group_state IS 'ESTABLISHED')) AND archived = :fromArchive AND isActive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END ORDER BY lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC; selectConversationDetailsByQualifiedId: 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 new file mode 100644 index 00000000000..db30fa8a9cd --- /dev/null +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq @@ -0,0 +1,48 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity; + +CREATE TABLE ConversationFolder ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + folder_type TEXT AS ConversationFolderTypeEntity NOT NULL +); + +CREATE TABLE LabeledConversation ( + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + folder_id TEXT NOT NULL, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (folder_id) REFERENCES ConversationFolder(id) ON DELETE CASCADE ON UPDATE CASCADE, + + PRIMARY KEY (conversation_id, folder_id) +); + +getConversationsFromFolder: +SELECT ConversationDetailsWithEvents.* +FROM LabeledConversation +JOIN ConversationDetailsWithEvents + ON LabeledConversation.conversation_id = ConversationDetailsWithEvents.qualifiedId +WHERE LabeledConversation.folder_id = :folderId + AND ConversationDetailsWithEvents.archived = 0 +ORDER BY + ConversationDetailsWithEvents.lastModifiedDate DESC, + name IS NULL, + name COLLATE NOCASE ASC; + +getFavoriteFolder: +SELECT * FROM ConversationFolder WHERE folder_type = 'FAVORITE' +LIMIT 1; + +upsertFolder: +INSERT INTO ConversationFolder(id, name, folder_type) +VALUES( ?, ?, ?) +ON CONFLICT(id) DO UPDATE SET +name = excluded.name, +folder_type = excluded.folder_type; + +insertLabeledConversation: +INSERT OR IGNORE INTO LabeledConversation(conversation_id, folder_id) +VALUES(?, ?); + +clearFolders: +DELETE FROM ConversationFolder; diff --git a/persistence/src/commonMain/db_user/migrations/91.sqm b/persistence/src/commonMain/db_user/migrations/91.sqm new file mode 100644 index 00000000000..b424e1cb704 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/91.sqm @@ -0,0 +1,19 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity; + +CREATE TABLE ConversationFolder ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + folder_type TEXT AS ConversationFolderTypeEntity NOT NULL +); + + +CREATE TABLE LabeledConversation ( + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + folder_id TEXT NOT NULL, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (folder_id) REFERENCES ConversationFolder(id) ON DELETE CASCADE ON UPDATE CASCADE, + + PRIMARY KEY (conversation_id, folder_id) +); diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt index dc815aa8c3d..464034de37a 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt @@ -57,7 +57,7 @@ interface ConversationDAO { suspend fun updateConversationReadDate(conversationID: QualifiedIDEntity, date: Instant) suspend fun updateAllConversationsNotificationDate() suspend fun getAllConversations(): Flow> - suspend fun getAllConversationDetails(fromArchive: Boolean): Flow> + suspend fun getAllConversationDetails(fromArchive: Boolean, filter: ConversationFilterEntity): Flow> suspend fun getAllConversationDetailsWithEvents( fromArchive: Boolean = false, onlyInteractionEnabled: Boolean = false, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index d184b79bacb..ef7b6ec58a1 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -214,8 +214,11 @@ internal class ConversationDAOImpl internal constructor( .flowOn(coroutineContext) } - override suspend fun getAllConversationDetails(fromArchive: Boolean): Flow> { - return conversationDetailsQueries.selectAllConversationDetails(fromArchive, conversationMapper::fromViewToModel) + override suspend fun getAllConversationDetails( + fromArchive: Boolean, + filter: ConversationFilterEntity + ): Flow> { + return conversationDetailsQueries.selectAllConversationDetails(fromArchive, filter.toString(), conversationMapper::fromViewToModel) .asFlow() .mapToList() .flowOn(coroutineContext) 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 new file mode 100644 index 00000000000..8c0e6f2ac84 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt @@ -0,0 +1,27 @@ +/* + * 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.persistence.dao.conversation.folder + +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import kotlinx.coroutines.flow.Flow + +interface ConversationFolderDAO { + suspend fun observeConversationListFromFolder(folderId: String): Flow> + suspend fun getFavoriteConversationFolder(): ConversationFolderEntity + suspend fun updateConversationFolders(folderWithConversationsList: List) +} 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 new file mode 100644 index 00000000000..06768bf0110 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt @@ -0,0 +1,73 @@ +/* + * 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.persistence.dao.conversation.folder + +import app.cash.sqldelight.coroutines.asFlow +import com.wire.kalium.persistence.ConversationFoldersQueries +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsMapper +import com.wire.kalium.persistence.util.mapToList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext + +class ConversationFolderDAOImpl internal constructor( + private val conversationFoldersQueries: ConversationFoldersQueries, + private val coroutineContext: CoroutineContext, +) : ConversationFolderDAO { + private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper + + override suspend fun observeConversationListFromFolder(folderId: String): Flow> { + return conversationFoldersQueries.getConversationsFromFolder( + folderId, + conversationDetailsWithEventsMapper::fromViewToModel + ) + .asFlow() + .mapToList() + .flowOn(coroutineContext) + } + + override suspend fun getFavoriteConversationFolder(): ConversationFolderEntity { + return conversationFoldersQueries.getFavoriteFolder { id, name, folderType -> + ConversationFolderEntity(id, name, folderType) + } + .executeAsOne() + } + + 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 -> + conversationFoldersQueries.upsertFolder( + folderWithConversations.id, + folderWithConversations.name, + folderWithConversations.type + ) + folderWithConversations.conversationIdList.forEach { conversationId -> + conversationFoldersQueries.insertLabeledConversation( + conversationId, + folderWithConversations.id + ) + } + } + } + } +} 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 new file mode 100644 index 00000000000..1bad61fbd08 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt @@ -0,0 +1,38 @@ +/* + * 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.persistence.dao.conversation.folder + +import com.wire.kalium.persistence.dao.QualifiedIDEntity + +data class ConversationFolderEntity( + val id: String, + val name: String, + val type: ConversationFolderTypeEntity +) + +data class FolderWithConversationsEntity( + val id: String, + val name: String, + val type: ConversationFolderTypeEntity, + val conversationIdList: List +) + +enum class ConversationFolderTypeEntity { + USER, + FAVORITE +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt index fbceb8364ea..f58f4f2b279 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt @@ -25,8 +25,10 @@ import com.wire.kalium.persistence.Call import com.wire.kalium.persistence.Client import com.wire.kalium.persistence.Connection import com.wire.kalium.persistence.Conversation +import com.wire.kalium.persistence.ConversationFolder import com.wire.kalium.persistence.ConversationLegalHoldStatusChangeNotified import com.wire.kalium.persistence.LastMessage +import com.wire.kalium.persistence.LabeledConversation import com.wire.kalium.persistence.Member import com.wire.kalium.persistence.Message import com.wire.kalium.persistence.MessageAssetContent @@ -271,4 +273,12 @@ internal object TableMapper { conversation_idAdapter = QualifiedIDAdapter, creation_dateAdapter = InstantTypeAdapter, ) + + val labeledConversationAdapter = LabeledConversation.Adapter( + conversation_idAdapter = QualifiedIDAdapter + ) + + val conversationFolderAdapter = ConversationFolder.Adapter( + folder_typeAdapter = EnumColumnAdapter() + ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index bc9fe1464c4..7c75ee447b6 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -58,6 +58,8 @@ import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAOImpl import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAOImpl import com.wire.kalium.persistence.dao.member.MemberDAO import com.wire.kalium.persistence.dao.member.MemberDAOImpl import com.wire.kalium.persistence.dao.member.MemberEntity @@ -159,6 +161,8 @@ class UserDatabaseBuilder internal constructor( MessageAssetTransferStatusAdapter = TableMapper.messageAssetTransferStatusAdapter, MessageDraftAdapter = TableMapper.messageDraftsAdapter, LastMessageAdapter = TableMapper.lastMessageAdapter, + LabeledConversationAdapter = TableMapper.labeledConversationAdapter, + ConversationFolderAdapter = TableMapper.conversationFolderAdapter ) init { @@ -201,6 +205,12 @@ class UserDatabaseBuilder internal constructor( queriesContext, ) + val conversationFolderDAO: ConversationFolderDAO + get() = ConversationFolderDAOImpl( + database.conversationFoldersQueries, + queriesContext + ) + private val conversationMembersCache = FlowCache>(databaseScope) diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConnectionDaoTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConnectionDaoTest.kt index 6ca8c5826ff..28333dfa8c5 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConnectionDaoTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConnectionDaoTest.kt @@ -20,7 +20,6 @@ package com.wire.kalium.persistence.dao import com.wire.kalium.persistence.BaseDatabaseTest import com.wire.kalium.persistence.db.UserDatabaseBuilder -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.toInstant @@ -28,7 +27,6 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -@OptIn(ExperimentalCoroutinesApi::class) class ConnectionDaoTest : BaseDatabaseTest() { private val connection1 = connectionEntity("1") 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 b98958884d4..6bb242bf28d 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 @@ -28,6 +28,7 @@ import com.wire.kalium.persistence.dao.client.ClientDAO import com.wire.kalium.persistence.dao.client.InsertClientParam import com.wire.kalium.persistence.dao.conversation.ConversationDAO import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity import com.wire.kalium.persistence.dao.conversation.ConversationGuestLinkEntity import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity import com.wire.kalium.persistence.dao.conversation.E2EIConversationClientInfoEntity @@ -132,11 +133,13 @@ class ConversationDAOTest : BaseDatabaseTest() { @Test fun givenExistingConversation_WhenReinserting_ThenGroupStateIsUpdated() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity2) - conversationDAO.insertConversation(conversationEntity2.copy( - protocolInfo = mlsProtocolInfo1.copy( - groupState = ConversationEntity.GroupState.PENDING_JOIN + conversationDAO.insertConversation( + conversationEntity2.copy( + protocolInfo = mlsProtocolInfo1.copy( + groupState = ConversationEntity.GroupState.PENDING_JOIN + ) ) - )) + ) val result = conversationDAO.getConversationById(conversationEntity2.id) assertEquals(ConversationEntity.GroupState.PENDING_JOIN, (result?.protocolInfo as ConversationEntity.ProtocolInfo.MLS).groupState) } @@ -998,7 +1001,7 @@ class ConversationDAOTest : BaseDatabaseTest() { conversationDAO.insertConversation(conversation) connectionDAO.insertConnection(connectionEntity) - conversationDAO.getAllConversationDetails(fromArchive).first().let { + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { assertEquals(1, it.size) val result = it.first() @@ -1033,7 +1036,7 @@ class ConversationDAOTest : BaseDatabaseTest() { conversationDAO.insertConversation(conversation) connectionDAO.insertConnection(connectionEntity) - conversationDAO.getAllConversationDetails(fromArchive).first().let { + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { assertEquals(1, it.size) } conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { @@ -1053,7 +1056,7 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMember(member1, conversationEntity1.id) memberDAO.insertMember(member2, conversationEntity1.id) - conversationDAO.getAllConversationDetails(fromArchive).first().let { + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { assertEquals(1, it.size) assertEquals(conversationEntity1.id, it.first().id) } @@ -1076,7 +1079,7 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMember(member1, conversationEntity1.id) memberDAO.insertMember(member2, conversationEntity2.id) - conversationDAO.getAllConversationDetails(fromArchive).first().let { + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { assertEquals(2, it.size) } conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { @@ -1097,7 +1100,7 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMember(member1, conversationEntity1.id) memberDAO.insertMember(member2, conversationEntity2.id) - conversationDAO.getAllConversationDetails(fromArchive).first().let { + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { assertEquals(2, it.size) } conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { @@ -1114,7 +1117,7 @@ class ConversationDAOTest : BaseDatabaseTest() { insertTeamUserAndMember(team, user1, conversation.id) // when - val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val result = conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first() val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then @@ -1132,7 +1135,7 @@ class ConversationDAOTest : BaseDatabaseTest() { insertTeamUserAndMember(team, user1, conversation.id) // when - val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val result = conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first() val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then @@ -1165,7 +1168,7 @@ class ConversationDAOTest : BaseDatabaseTest() { insertTeamUserAndMember(team, user1, conversation2.id) // when - val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val result = conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first() val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then @@ -1202,7 +1205,7 @@ class ConversationDAOTest : BaseDatabaseTest() { insertTeamUserAndMember(team, user1, conversation2.id) // when - val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val result = conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first() val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then @@ -1278,7 +1281,10 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMembersWithQualifiedId(listOf(member1, member2), conversationEntity1.id) memberDAO.insertMembersWithQualifiedId(listOf(member1, member2), conversationEntity2.id) - conversationDAO.getAllConversationDetails(fromArchive = false).first().let { + conversationDAO.getAllConversationDetails( + fromArchive = false, + filter = ConversationFilterEntity.ALL + ).first().let { assertEquals(1, it.size) assertEquals(conversationEntity1.id, it.first().id) } 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 new file mode 100644 index 00000000000..273c1da825b --- /dev/null +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt @@ -0,0 +1,102 @@ +/* + * 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.persistence.dao.conversation.folder + +import com.wire.kalium.persistence.BaseDatabaseTest +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.UserIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.member.MemberEntity +import com.wire.kalium.persistence.db.UserDatabaseBuilder +import com.wire.kalium.persistence.utils.stubs.newConversationEntity +import com.wire.kalium.persistence.utils.stubs.newUserEntity +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConversationFolderDAOTest : BaseDatabaseTest() { + + private val conversationEntity1 = newConversationEntity("Test1").copy(type = ConversationEntity.Type.GROUP) + private val userEntity1 = newUserEntity("userEntity1") + val member1 = MemberEntity(userEntity1.id, MemberEntity.Role.Admin) + + lateinit var db: UserDatabaseBuilder + private val selfUserId = UserIDEntity("selfValue", "selfDomain") + + @BeforeTest + fun setUp() { + deleteDatabase(selfUserId) + db = createDatabase(selfUserId, encryptedDBSecret, true) + } + + @Test + fun givenFolderWithConversationId_WhenObservingThenConversationShouldBeReturned() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folderId = "folderId1" + + val conversationFolderEntity = folderWithConversationsEntity( + id = folderId, + name = "folderName", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id)) + + db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity)) + val result = db.conversationFolderDAO.observeConversationListFromFolder(folderId).first().first() + + assertEquals(conversationEntity1.id, result.conversationViewEntity.id) + } + + @Test + fun givenFavoriteFolderWithConversationId_WhenObservingThenFavoriteConversationShouldBeReturned() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folderId = "folderId1" + + val conversationFolderEntity = folderWithConversationsEntity( + id = folderId, + name = "", + type = ConversationFolderTypeEntity.FAVORITE, + conversationIdList = listOf(conversationEntity1.id)) + + db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity)) + val result = db.conversationFolderDAO.getFavoriteConversationFolder() + + assertEquals(folderId, result.id) + } + + companion object { + fun folderWithConversationsEntity( + id: String = "folderId", + type: ConversationFolderTypeEntity = ConversationFolderTypeEntity.FAVORITE, + name: String = "", + conversationIdList: List = listOf(QualifiedIDEntity("conversationId", "domain")) + ) = FolderWithConversationsEntity( + id = id, + type = type, + name = name, + conversationIdList = conversationIdList + ) + } +}