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 1702a0acd83..c0aed853c10 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 @@ -224,6 +224,7 @@ interface ConversationRepository { ): Either suspend fun deleteConversation(conversationId: ConversationId): Either + suspend fun deleteLocalConversation(conversationId: ConversationId): Either /** * Deletes all conversation messages @@ -884,6 +885,12 @@ internal class ConversationDataSource internal constructor( } } + override suspend fun deleteLocalConversation(conversationId: ConversationId): Either { + return wrapStorageRequest { + conversationDAO.deleteConversationByQualifiedID(conversationId.toDao()) + } + } + override suspend fun clearContent(conversationId: ConversationId): Either = wrapStorageRequest { conversationDAO.clearContent(conversationId.toDao()) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt index dd0f47b5996..d80f96c3ba7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt @@ -253,6 +253,10 @@ internal interface MessageRepository { messageId: String, conversationId: ConversationId ): Either + + suspend fun getAllAssetIdsFromConversationId( + conversationId: ConversationId, + ): Either> } // TODO: suppress TooManyFunctions for now, something we need to fix in the future @@ -706,4 +710,12 @@ internal class MessageDataSource internal constructor( ): Either = wrapStorageRequest { messageDAO.getMessageAssetTransferStatus(messageId, conversationId.toDao()).toModel() } + + override suspend fun getAllAssetIdsFromConversationId( + conversationId: ConversationId + ): Either> { + return wrapStorageRequest { + messageDAO.getAllMessageAssetIdsForConversationId(conversationId = conversationId.toDao()) + } + } } 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 9c031870119..1316044d8f3 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 @@ -1806,7 +1806,9 @@ class UserSessionScope internal constructor( this, userScopedLogger, refreshUsersWithoutMetadata, - sessionManager.getServerConfig().links + sessionManager.getServerConfig().links, + messages.messageRepository, + assetRepository ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearLocalConversationAssetsUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearLocalConversationAssetsUseCase.kt new file mode 100644 index 00000000000..71b244cc7c2 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearLocalConversationAssetsUseCase.kt @@ -0,0 +1,51 @@ +/* + * 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 + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.asset.AssetRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap + +interface ClearLocalConversationAssetsUseCase { + /** + * Clear all conversation assets from local storage + * + * @param conversationId - id of conversation in which assets should be cleared + */ + suspend operator fun invoke(conversationId: ConversationId): Either +} + +internal class ClearLocalConversationAssetsUseCaseImpl( + private val messageRepository: MessageRepository, + private val assetRepository: AssetRepository +) : ClearLocalConversationAssetsUseCase { + override suspend fun invoke(conversationId: ConversationId): Either { + return messageRepository.getAllAssetIdsFromConversationId(conversationId) + .flatMap { ids -> + if (ids.isEmpty()) return Either.Right(Unit) + + ids.map { id -> assetRepository.deleteAssetLocally(id) } + .reduce { acc, either -> + acc.flatMap { either } + } + } + } +} 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 bc65552ad2d..3a8b8f91d41 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 @@ -23,6 +23,7 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logic.cache.SelfConversationIdProvider import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.configuration.server.ServerConfigRepository +import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.connection.ConnectionRepository import com.wire.kalium.logic.data.conversation.ConversationGroupRepository import com.wire.kalium.logic.data.conversation.ConversationRepository @@ -38,6 +39,7 @@ import com.wire.kalium.logic.data.conversation.folders.ConversationFolderReposit import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.message.MessageRepository import com.wire.kalium.logic.data.message.PersistMessageUseCase import com.wire.kalium.logic.data.properties.UserPropertyRepository import com.wire.kalium.logic.data.team.TeamRepository @@ -115,6 +117,8 @@ class ConversationScope internal constructor( private val kaliumLogger: KaliumLogger, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val serverConfigLinks: ServerConfig.Links, + internal val messageRepository: MessageRepository, + internal val assetRepository: AssetRepository, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { @@ -266,6 +270,18 @@ class ConversationScope internal constructor( selfConversationIdProvider ) + val clearConversationAssetsLocally: ClearLocalConversationAssetsUseCase + get() = ClearLocalConversationAssetsUseCaseImpl( + messageRepository, + assetRepository + ) + + val deleteLocalConversationUseCase: DeleteLocalConversationUseCase + get() = DeleteLocalConversationUseCaseImpl( + conversationRepository, + clearConversationAssetsLocally + ) + val joinConversationViaCode: JoinConversationViaCodeUseCase get() = JoinConversationViaCodeUseCase(conversationGroupRepository, selfUserId) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteLocalConversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteLocalConversationUseCase.kt new file mode 100644 index 00000000000..903e7b11d5f --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteLocalConversationUseCase.kt @@ -0,0 +1,48 @@ +/* + * 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 + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap + +interface DeleteLocalConversationUseCase { + /** + * Delete local conversation which: + * - Clear all local assets + * - Clear content + * - Remove conversation + * + * @param conversationId - id of conversation to delete + */ + suspend operator fun invoke(conversationId: ConversationId): Either +} + +internal class DeleteLocalConversationUseCaseImpl( + private val conversationRepository: ConversationRepository, + private val clearLocalConversationAssets: ClearLocalConversationAssetsUseCase +) : DeleteLocalConversationUseCase { + + override suspend fun invoke(conversationId: ConversationId): Either { + return clearLocalConversationAssets(conversationId) + .flatMap { conversationRepository.clearContent(conversationId) } + .flatMap { conversationRepository.deleteLocalConversation(conversationId) } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearLocalConversationAssetsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearLocalConversationAssetsUseCaseTest.kt new file mode 100644 index 00000000000..b3d9d4db664 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearLocalConversationAssetsUseCaseTest.kt @@ -0,0 +1,111 @@ +/* + * 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 + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.asset.AssetRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageRepository +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.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class ClearLocalConversationAssetsUseCaseTest { + + @Test + fun givenConversationAssetIds_whenAllDeletionsAreSuccess_thenSuccessResultIsPropagated() = runTest { + // given + val ids = listOf("id_1", "id_2") + val (arrangement, useCase) = Arrangement() + .withAssetIdsResponse(ids) + .withAssetClearSuccess("id_1") + .withAssetClearSuccess("id_2") + .arrange() + + // when + val result = useCase(ConversationId("someValue", "someDomain")) + + // then + assertIs>(result) + coVerify { arrangement.assetRepository.deleteAssetLocally(any()) }.wasInvoked(exactly = 2) + } + + @Test + fun givenConversationAssetIds_whenOneDeletionFailed_thenFailureResultIsPropagated() = runTest { + // given + val ids = listOf("id_1", "id_2") + val (arrangement, useCase) = Arrangement() + .withAssetIdsResponse(ids) + .withAssetClearSuccess("id_1") + .withAssetClearError("id_2") + .arrange() + + // when + val result = useCase(ConversationId("someValue", "someDomain")) + + // then + assertIs>(result) + coVerify { arrangement.assetRepository.deleteAssetLocally(any()) }.wasInvoked(exactly = 2) + } + + @Test + fun givenEmptyConversationAssetIds_whenInvoked_thenDeletionsAreNotInvoked() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withAssetIdsResponse(emptyList()) + .arrange() + + // when + val result = useCase(ConversationId("someValue", "someDomain")) + + // then + assertIs>(result) + coVerify { arrangement.assetRepository.deleteAssetLocally(any()) }.wasNotInvoked() + } + + private class Arrangement { + @Mock + val messageRepository = mock(MessageRepository::class) + + @Mock + val assetRepository = mock(AssetRepository::class) + + suspend fun withAssetClearSuccess(id: String) = apply { + coEvery { assetRepository.deleteAssetLocally(id) }.returns(Either.Right(Unit)) + } + + suspend fun withAssetClearError(id: String) = apply { + coEvery { assetRepository.deleteAssetLocally(id) }.returns(Either.Left(CoreFailure.Unknown(null))) + } + + suspend fun withAssetIdsResponse(ids: List) = apply { + coEvery { messageRepository.getAllAssetIdsFromConversationId(any()) }.returns(Either.Right(ids)) + } + + fun arrange() = this to ClearLocalConversationAssetsUseCaseImpl( + messageRepository = messageRepository, + assetRepository = assetRepository + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteLocalConversationUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteLocalConversationUseCaseTest.kt new file mode 100644 index 00000000000..ed3c73272e1 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteLocalConversationUseCaseTest.kt @@ -0,0 +1,137 @@ +/* + * 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 + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +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.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class DeleteLocalConversationUseCaseTest { + + companion object { + val SUCCESS = Either.Right(Unit) + val ERROR = Either.Left(CoreFailure.Unknown(null)) + val CONVERSATION_ID = ConversationId("someValue", "someDomain") + } + + @Test + fun givenDeleteLocalConversationInvoked_whenAllStepsAreSuccessful_thenSuccessResultIsPropagated() = runTest { + // given + val (_, useCase) = Arrangement() + .withClearContent(SUCCESS) + .withClearLocalAsset(SUCCESS) + .withDeleteLocalConversation(SUCCESS) + .arrange() + + // when + val result = useCase(CONVERSATION_ID) + + // then + assertIs>(result) + } + + @Test + fun givenDeleteLocalConversationInvoked_whenAssetClearIsUnsuccessful_thenErrorResultIsPropagated() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withClearContent(SUCCESS) + .withClearLocalAsset(ERROR) + .withDeleteLocalConversation(SUCCESS) + .arrange() + + // when + val result = useCase(CONVERSATION_ID) + + // then + assertIs>(result) + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasNotInvoked() + coVerify { arrangement.conversationRepository.deleteLocalConversation(any()) }.wasNotInvoked() + } + + @Test + fun givenDeleteLocalConversationInvoked_whenContentClearIsUnsuccessful_thenErrorResultIsPropagated() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withClearContent(ERROR) + .withClearLocalAsset(SUCCESS) + .withDeleteLocalConversation(SUCCESS) + .arrange() + + // when + val result = useCase(CONVERSATION_ID) + + // then + assertIs>(result) + coVerify { arrangement.clearLocalConversationAssets(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.deleteLocalConversation(any()) }.wasNotInvoked() + } + + @Test + fun givenDeleteLocalConversationInvoked_whenDeleteConversationIsUnsuccessful_thenErrorResultIsPropagated() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withClearContent(SUCCESS) + .withClearLocalAsset(SUCCESS) + .withDeleteLocalConversation(ERROR) + .arrange() + + // when + val result = useCase(CONVERSATION_ID) + + // then + assertIs>(result) + coVerify { arrangement.clearLocalConversationAssets(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.deleteLocalConversation(any()) }.wasInvoked(exactly = 1) + } + + private class Arrangement { + @Mock + val conversationRepository = mock(ConversationRepository::class) + + @Mock + val clearLocalConversationAssets = mock(ClearLocalConversationAssetsUseCase::class) + + suspend fun withClearContent(result: Either) = apply { + coEvery { conversationRepository.clearContent(any()) }.returns(result) + } + + suspend fun withDeleteLocalConversation(result: Either) = apply { + coEvery { conversationRepository.deleteLocalConversation(any()) }.returns(result) + } + + suspend fun withClearLocalAsset(result: Either) = apply { + coEvery { clearLocalConversationAssets(any()) }.returns(result) + } + + fun arrange() = this to DeleteLocalConversationUseCaseImpl( + conversationRepository = conversationRepository, + clearLocalConversationAssets = clearLocalConversationAssets + ) + } +} diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAssetView.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAssetView.sq index 83085dafdbf..ee88efd383f 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAssetView.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageAssetView.sq @@ -81,3 +81,13 @@ AND assetMimeType NOT IN :mimeTypes AND assetId IS NOT NULL AND expireAfterMillis IS NULL ORDER BY date DESC; + +getAllAssetMessagesByConversationId: +SELECT assetId FROM MessageAssetView +WHERE conversationId = :conversationId +AND visibility IN :visibility +AND contentType IN :contentTypes +AND assetId IS NOT NULL +AND isEphemeral = FALSE +AND dataPath IS NOT NULL +ORDER BY date DESC; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt index e37edbceea0..7310c09d2fa 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt @@ -161,4 +161,5 @@ interface MessageDAO { suspend fun observeAssetStatuses(conversationId: QualifiedIDEntity): Flow> suspend fun getMessageAssetTransferStatus(messageId: String, conversationId: QualifiedIDEntity): AssetTransferStatusEntity + suspend fun getAllMessageAssetIdsForConversationId(conversationId: QualifiedIDEntity): List } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index 06d1f66ba3d..31cf29ceb38 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -505,6 +505,18 @@ internal class MessageDAOImpl internal constructor( .executeAsOne() } + override suspend fun getAllMessageAssetIdsForConversationId( + conversationId: QualifiedIDEntity + ): List { + return withContext(coroutineContext) { + assetViewQueries.getAllAssetMessagesByConversationId( + conversationId, + listOf(MessageEntity.Visibility.VISIBLE), + listOf(MessageEntity.ContentType.ASSET) + ).executeAsList().mapNotNull { it.assetId } + } + } + override val platformExtensions: MessageExtensions = MessageExtensionsImpl(queries, assetViewQueries, mapper, coroutineContext) }