diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt index 56103c2371f..b386f096073 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt @@ -259,7 +259,8 @@ sealed interface MessageContent { data class Cleared( val conversationId: ConversationId, - val time: Instant + val time: Instant, + val needToRemoveLocally: Boolean ) : Signaling // server message content types diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt index 036fa81bf0f..31013c6e633 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt @@ -574,13 +574,15 @@ class ProtoContentMapperImpl( Cleared( conversationId = readableContent.conversationId.value, qualifiedConversationId = idMapper.toProtoModel(readableContent.conversationId), - clearedTimestamp = readableContent.time.toEpochMilliseconds() + clearedTimestamp = readableContent.time.toEpochMilliseconds(), + needToRemoveLocally = readableContent.needToRemoveLocally ) ) private fun unpackCleared(protoContent: GenericMessage.Content.Cleared) = MessageContent.Cleared( conversationId = extractConversationId(protoContent.value.qualifiedConversationId, protoContent.value.conversationId), - time = Instant.fromEpochMilliseconds(protoContent.value.clearedTimestamp) + time = Instant.fromEpochMilliseconds(protoContent.value.clearedTimestamp), + needToRemoveLocally = protoContent.value.needToRemoveLocally ?: false ) private fun toProtoLegalHoldStatus(legalHoldStatus: Conversation.LegalHoldStatus): LegalHoldStatus = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt index 27c20d7eeff..d884b86693f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt @@ -66,7 +66,8 @@ internal class ClearConversationContentUseCaseImpl( id = uuid4().toString(), content = MessageContent.Cleared( conversationId = conversationId, - time = DateTimeUtil.currentInstant() + time = DateTimeUtil.currentInstant(), + needToRemoveLocally = false // TODO Handle in upcoming tasks ), // sending the message to clear this conversation conversationId = selfConversationId, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt index 83402b724f5..3f4ef55ffdf 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt @@ -41,11 +41,18 @@ internal class ClearConversationContentHandlerImpl( message: Message.Signaling, messageContent: MessageContent.Cleared ) { - val isMessageComingFromOtherClient = message.senderUserId == selfUserId + val isMessageComingFromOtherClient = message.senderUserId != selfUserId val isMessageDestinedForSelfConversation: Boolean = isMessageSentInSelfConversation(message) - if (isMessageComingFromOtherClient && isMessageDestinedForSelfConversation) { - conversationRepository.clearContent(messageContent.conversationId) + if (isMessageComingFromOtherClient) { + when { + !messageContent.needToRemoveLocally && !isMessageDestinedForSelfConversation -> return + messageContent.needToRemoveLocally && !isMessageDestinedForSelfConversation -> conversationRepository.deleteConversation( + messageContent.conversationId + ) + + else -> conversationRepository.clearContent(messageContent.conversationId) + } } } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt new file mode 100644 index 00000000000..39ce8e1d7ad --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt @@ -0,0 +1,212 @@ +/* + * 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.sync.receiver.conversation + +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.IsMessageSentInSelfConversationUseCase +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.sync.receiver.handler.ClearConversationContentHandler +import com.wire.kalium.logic.sync.receiver.handler.ClearConversationContentHandlerImpl +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import kotlin.test.Test + +class ClearConversationContentHandlerTest { + + @Test + fun givenMessageFromOtherClient_whenMessageNeedsToBeRemovedLocallyAndUserIsNotPartOfConversation_thenWholeConversationShouldBeDeleted() = + runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(false) + .arrange() + + // when + handler.handle( + message = MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = true + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasInvoked(exactly = once) + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasNotInvoked() + } + + @Test + fun givenMessageFromOtherClient_whenMessageNeedsToBeRemovedLocallyAndUserIsPartOfConversation_thenOnlyContentShouldBeCleared() = + runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(true) + .arrange() + + // when + handler.handle( + message = MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = true + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasInvoked(exactly = once) + } + + @Test + fun givenMessageFromOtherClient_whenMessageDoesNotNeedToBeRemovedAndUserIsNotPartOfConversation_thenContentNorConversationShouldBeRemoved() = + runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(false) + .arrange() + + // when + handler.handle( + message = MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = false + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasNotInvoked() + } + + @Test + fun givenMessageFromOtherClient_whenMessageDoesNotNeedToBeRemovedAndUserIsPartOfConversation_thenContentShouldBeRemoved() = runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(true) + .arrange() + + // when + handler.handle( + message = MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = false + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasInvoked(exactly = once) + } + + @Test + fun givenMessageFromTheSameClient_whenHandleIsInvoked_thenContentNorConversationShouldBeRemoved() = runTest { + // given + val (arrangement, handler) = Arrangement() + .withMessageSentInSelfConversation(true) + .arrange() + + // when + handler.handle( + message = OWN_MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = false + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) } + .wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) } + .wasNotInvoked() + } + + + private class Arrangement { + @Mock + val conversationRepository = mock(ConversationRepository::class) + + @Mock + val isMessageSentInSelfConversationUseCase = mock(IsMessageSentInSelfConversationUseCase::class) + + suspend fun withMessageSentInSelfConversation(isSentInSelfConv: Boolean) = apply { + coEvery { isMessageSentInSelfConversationUseCase(any()) }.returns(isSentInSelfConv) + } + + suspend fun arrange(): Pair = + this to ClearConversationContentHandlerImpl( + conversationRepository = conversationRepository, + selfUserId = TestUser.USER_ID, + isMessageSentInSelfConversation = isMessageSentInSelfConversationUseCase, + ).apply { + coEvery { conversationRepository.deleteConversation(any()) }.returns(Either.Right(Unit)) + coEvery { conversationRepository.clearContent(any()) }.returns(Either.Right(Unit)) + } + } + + companion object { + private val CONVERSATION_ID = ConversationId("conversationId", "domain") + private val OTHER_USER_ID = UserId("otherUserId", "domain") + + private val MESSAGE_CONTENT = MessageContent.DataTransfer( + trackingIdentifier = MessageContent.DataTransfer.TrackingIdentifier( + identifier = "abcd-1234-efgh-5678" + ) + ) + val MESSAGE = Message.Signaling( + id = "messageId", + content = MESSAGE_CONTENT, + conversationId = CONVERSATION_ID, + date = Instant.DISTANT_PAST, + senderUserId = OTHER_USER_ID, + senderClientId = ClientId("deviceId"), + status = Message.Status.Sent, + isSelfMessage = false, + expirationData = null, + ) + + val OWN_MESSAGE = MESSAGE.copy(senderUserId = TestUser.USER_ID) + } +} diff --git a/protobuf-codegen/src/main/proto/messages.proto b/protobuf-codegen/src/main/proto/messages.proto index efd576ac5b9..9a08423e27f 100644 --- a/protobuf-codegen/src/main/proto/messages.proto +++ b/protobuf-codegen/src/main/proto/messages.proto @@ -185,6 +185,7 @@ message Cleared { required int64 cleared_timestamp = 2; // only optional to maintain backwards compatibility optional QualifiedConversationId qualified_conversation_id = 3; + optional bool needToRemoveLocally = 4 [default = false]; } message MessageHide {