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..2820fa3d8df 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 deleteConversationLocally(conversationId: ConversationId): Either /** * Deletes all conversation messages @@ -884,6 +885,12 @@ internal class ConversationDataSource internal constructor( } } + override suspend fun deleteConversationLocally(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 0e1902baba4..c6281fd79f0 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 @@ -254,6 +254,10 @@ internal interface MessageRepository { conversationId: ConversationId ): Either + suspend fun getAllAssetIdsFromConversationId( + conversationId: ConversationId, + ): Either> + suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either suspend fun getNextAudioMessageInConversation(conversationId: ConversationId, messageId: String): Either } @@ -710,6 +714,14 @@ internal class MessageDataSource internal constructor( messageDAO.getMessageAssetTransferStatus(messageId, conversationId.toDao()).toModel() } + override suspend fun getAllAssetIdsFromConversationId( + conversationId: ConversationId + ): Either> { + return wrapStorageRequest { + messageDAO.getAllMessageAssetIdsForConversationId(conversationId = conversationId.toDao()) + } + } + override suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): Either = wrapStorageRequest { messageDAO.getSenderNameById(messageId, 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 9045af9fa76..81819a6de51 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 @@ -1809,7 +1809,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/ClearConversationAssetsLocallyUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCase.kt new file mode 100644 index 00000000000..9a545184971 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCase.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 ClearConversationAssetsLocallyUseCase { + /** + * 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 ClearConversationAssetsLocallyUseCaseImpl( + private val messageRepository: MessageRepository, + private val assetRepository: AssetRepository +) : ClearConversationAssetsLocallyUseCase { + 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..988f49480e3 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: ClearConversationAssetsLocallyUseCase + get() = ClearConversationAssetsLocallyUseCaseImpl( + messageRepository, + assetRepository + ) + + val deleteConversationLocallyUseCase: DeleteConversationLocallyUseCase + get() = DeleteConversationLocallyUseCaseImpl( + conversationRepository, + clearConversationAssetsLocally + ) + val joinConversationViaCode: JoinConversationViaCodeUseCase get() = JoinConversationViaCodeUseCase(conversationGroupRepository, selfUserId) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt new file mode 100644 index 00000000000..aaf07b999b3 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.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 DeleteConversationLocallyUseCase { + /** + * 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 DeleteConversationLocallyUseCaseImpl( + private val conversationRepository: ConversationRepository, + private val clearLocalConversationAssets: ClearConversationAssetsLocallyUseCase +) : DeleteConversationLocallyUseCase { + + override suspend fun invoke(conversationId: ConversationId): Either { + return clearLocalConversationAssets(conversationId) + .flatMap { conversationRepository.clearContent(conversationId) } + .flatMap { conversationRepository.deleteConversationLocally(conversationId) } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCaseTest.kt new file mode 100644 index 00000000000..2585230f30d --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationAssetsLocallyUseCaseTest.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 ClearConversationAssetsLocallyUseCaseTest { + + @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 ClearConversationAssetsLocallyUseCaseImpl( + messageRepository = messageRepository, + assetRepository = assetRepository + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt new file mode 100644 index 00000000000..a6abd5c4edf --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.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 DeleteConversationLocallyUseCaseTest { + + 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.deleteConversationLocally(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.deleteConversationLocally(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.deleteConversationLocally(any()) }.wasInvoked(exactly = 1) + } + + private class Arrangement { + @Mock + val conversationRepository = mock(ConversationRepository::class) + + @Mock + val clearLocalConversationAssets = mock(ClearConversationAssetsLocallyUseCase::class) + + suspend fun withClearContent(result: Either) = apply { + coEvery { conversationRepository.clearContent(any()) }.returns(result) + } + + suspend fun withDeleteLocalConversation(result: Either) = apply { + coEvery { conversationRepository.deleteConversationLocally(any()) }.returns(result) + } + + suspend fun withClearLocalAsset(result: Either) = apply { + coEvery { clearLocalConversationAssets(any()) }.returns(result) + } + + fun arrange() = this to DeleteConversationLocallyUseCaseImpl( + 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..59762233238 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,11 @@ 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 contentType IN :contentTypes +AND assetId IS NOT NULL +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 25884bfffc3..86963c7b9ba 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,6 +161,7 @@ interface MessageDAO { suspend fun observeAssetStatuses(conversationId: QualifiedIDEntity): Flow> suspend fun getMessageAssetTransferStatus(messageId: String, conversationId: QualifiedIDEntity): AssetTransferStatusEntity + suspend fun getAllMessageAssetIdsForConversationId(conversationId: QualifiedIDEntity): List suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String? suspend fun getNextAudioMessageInConversation(prevMessageId: String, conversationId: QualifiedIDEntity): String? } 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 40f89f6068f..a27ccb945d3 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 @@ -507,6 +507,17 @@ internal class MessageDAOImpl internal constructor( .executeAsOne() } + override suspend fun getAllMessageAssetIdsForConversationId( + conversationId: QualifiedIDEntity + ): List { + return withContext(coroutineContext) { + assetViewQueries.getAllAssetMessagesByConversationId( + conversationId, + listOf(MessageEntity.ContentType.ASSET) + ).executeAsList().mapNotNull { it.assetId } + } + } + override suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String? = withContext(coroutineContext) { userQueries.selectNameByMessageId(id, conversationId).executeAsOneOrNull()?.name }