Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: notify user when sending first message in conversation on legal hold [WPB-4566] #2315

Merged
merged 14 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,13 @@ interface ConversationRepository {
suspend fun updateLegalHoldStatus(
conversationId: ConversationId,
legalHoldStatus: Conversation.LegalHoldStatus
): Either<CoreFailure, Unit>
): Either<CoreFailure, Boolean>

suspend fun setLegalHoldStatusChangeNotified(conversationId: ConversationId): Either<CoreFailure, Boolean>

suspend fun observeLegalHoldStatus(conversationId: ConversationId): Flow<Either<StorageFailure, Conversation.LegalHoldStatus>>

suspend fun observeLegalHoldForConversation(conversationId: ConversationId): Flow<Either<StorageFailure, Conversation.LegalHoldStatus>>
suspend fun observeLegalHoldStatusChangeNotified(conversationId: ConversationId): Flow<Either<StorageFailure, Boolean>>
}

@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
Expand Down Expand Up @@ -1077,20 +1081,36 @@ internal class ConversationDataSource internal constructor(
override suspend fun updateLegalHoldStatus(
conversationId: ConversationId,
legalHoldStatus: Conversation.LegalHoldStatus
): Either<CoreFailure, Unit> {
): Either<CoreFailure, Boolean> {
val legalHoldStatusEntity = conversationMapper.legalHoldStatusToEntity(legalHoldStatus)
return wrapStorageRequest {
conversationDAO.updateLegalHoldStatus(
conversationId = conversationId.toDao(),
legalHoldStatus = legalHoldStatusEntity
)
conversationId.toDao().let { conversationIdEntity ->
conversationDAO.updateLegalHoldStatus(
conversationId = conversationIdEntity,
legalHoldStatus = legalHoldStatusEntity
).also { legalHoldUpdated ->
if (legalHoldUpdated) {
conversationDAO.updateLegalHoldStatusChangeNotified(conversationId = conversationIdEntity, notified = false)
}
}
}
}
}
override suspend fun setLegalHoldStatusChangeNotified(conversationId: ConversationId): Either<CoreFailure, Boolean> =
wrapStorageRequest {
conversationDAO.updateLegalHoldStatusChangeNotified(conversationId = conversationId.toDao(), notified = true)
}

override suspend fun observeLegalHoldForConversation(conversationId: ConversationId) =
conversationDAO.observeLegalHoldForConversation(conversationId.toDao())
override suspend fun observeLegalHoldStatus(conversationId: ConversationId) =
conversationDAO.observeLegalHoldStatus(conversationId.toDao())
.map { conversationMapper.legalHoldStatusFromEntity(it) }
.wrapStorageRequest()
.distinctUntilChanged()

override suspend fun observeLegalHoldStatusChangeNotified(conversationId: ConversationId): Flow<Either<StorageFailure, Boolean>> =
conversationDAO.observeLegalHoldStatusChangeNotified(conversationId.toDao())
.wrapStorageRequest()
.distinctUntilChanged()

companion object {
const val DEFAULT_MEMBER_ROLE = "wire_member"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,5 +318,9 @@ class ConversationScope internal constructor(
get() = SetUserInformedAboutVerificationUseCaseImpl(conversationRepository)
val observeInformAboutVerificationBeforeMessagingFlagUseCase: ObserveDegradedConversationNotifiedUseCase
get() = ObserveDegradedConversationNotifiedUseCaseImpl(conversationRepository)
val setNotifiedAboutConversationUnderLegalHold: SetNotifiedAboutConversationUnderLegalHoldUseCase
get() = SetNotifiedAboutConversationUnderLegalHoldUseCaseImpl(conversationRepository)
val observeConversationUnderLegalHoldNotified: ObserveConversationUnderLegalHoldNotifiedUseCase
get() = ObserveConversationUnderLegalHoldNotifiedUseCaseImpl(conversationRepository)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Wire
* Copyright (C) 2023 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.data.conversation.Conversation
import com.wire.kalium.logic.data.conversation.ConversationRepository
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.functional.flatMapRightWithEither
import com.wire.kalium.logic.functional.mapRight
import com.wire.kalium.logic.functional.mapToRightOr
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged

/**
* UseCase for observing if User was notified about conversation being subject of legal hold
*/
interface ObserveConversationUnderLegalHoldNotifiedUseCase {
suspend operator fun invoke(conversationId: ConversationId): Flow<Boolean>
}

internal class ObserveConversationUnderLegalHoldNotifiedUseCaseImpl internal constructor(
private val conversationRepository: ConversationRepository
) : ObserveConversationUnderLegalHoldNotifiedUseCase {

override suspend fun invoke(conversationId: ConversationId): Flow<Boolean> =
conversationRepository.observeLegalHoldStatus(conversationId)
.flatMapRightWithEither { legalHoldStatus ->
conversationRepository.observeLegalHoldStatusChangeNotified(conversationId)
.mapRight { isUserNotifiedAboutStatusChange ->
when (legalHoldStatus) {
Conversation.LegalHoldStatus.ENABLED -> isUserNotifiedAboutStatusChange
else -> true // we only need to notify if legal hold was enabled
}
}
}
.mapToRightOr(true)
.distinctUntilChanged()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Wire
* Copyright (C) 2023 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.data.conversation.ConversationRepository
import com.wire.kalium.logic.data.id.ConversationId

/**
* UseCase for setting legal_hold_change_notified flag to true,
* it means that User was notified about the recent change in legal hold status.
*/
interface SetNotifiedAboutConversationUnderLegalHoldUseCase {
suspend operator fun invoke(conversationId: ConversationId)
}

internal class SetNotifiedAboutConversationUnderLegalHoldUseCaseImpl internal constructor(
private val conversationRepository: ConversationRepository
) : SetNotifiedAboutConversationUnderLegalHoldUseCase {
override suspend fun invoke(conversationId: ConversationId) {
conversationRepository.setLegalHoldStatusChangeNotified(conversationId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class MLSMessageCreatorImpl(
else -> false
}

val legalHoldStatus = conversationRepository.observeLegalHoldForConversation(
val legalHoldStatus = conversationRepository.observeLegalHoldStatus(
message.conversationId
).first().let {
legalHoldStatusMapper.mapLegalHoldConversationStatus(it, message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class MessageEnvelopeCreatorImpl(
else -> false
}

val legalHoldStatus = conversationRepository.observeLegalHoldForConversation(
val legalHoldStatus = conversationRepository.observeLegalHoldStatus(
message.conversationId
).first().let {
legalHoldStatusMapper.mapLegalHoldConversationStatus(it, message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1321,28 +1321,77 @@ class ConversationRepositoryTest {
}

@Test
fun givenLegalHoldStatus_whenUpdateIsCalled_thenInvokeUpdateLegalHoldStatusFromOnce() = runTest {
fun givenLegalHoldStatus_whenUpdateIsCalled_thenInvokeUpdateLegalHoldStatusOnce() = runTest {
// given
val (arrange, conversationRepository) = Arrangement()
.withUpdateLegalHoldStatus(true)
.withUpdateLegalHoldStatusChangeNotified(true)
.arrange()

// when
conversationRepository.updateLegalHoldStatus(CONVERSATION_ID, Conversation.LegalHoldStatus.ENABLED)

// then
verify(arrange.conversationDAO)
.suspendFunction(arrange.conversationDAO::updateLegalHoldStatus)
.with(eq(CONVERSATION_ID.toDao()), any())
.wasInvoked(exactly = once)
}

@Test
fun givenConversationId_whenObservingLegalHoldStatus_thenInvokeObserveLegalHoldStatusFromOnce() = runTest {
fun givenLegalHoldStatusUpdated_whenUpdateChangeNotifiedIsCalled_thenInvokeUpdateLegalHoldStatusChangeNotifiedOnce() = runTest {
// given
val (arrange, conversationRepository) = Arrangement()
.withUpdateLegalHoldStatus(true)
.withUpdateLegalHoldStatusChangeNotified(true)
.arrange()
// when
conversationRepository.updateLegalHoldStatus(CONVERSATION_ID, Conversation.LegalHoldStatus.ENABLED)
// then
verify(arrange.conversationDAO)
.suspendFunction(arrange.conversationDAO::updateLegalHoldStatusChangeNotified)
.with(eq(CONVERSATION_ID.toDao()), any())
.wasInvoked(exactly = once)
}

@Test
fun givenLegalHoldStatusNotUpdated_whenUpdateChangeNotifiedIsCalled_thenDoNotInvokeUpdateLegalHoldStatusChangeNotified() = runTest {
// given
val (arrange, conversationRepository) = Arrangement()
.withUpdateLegalHoldStatus(false)
.withUpdateLegalHoldStatusChangeNotified(false)
.arrange()
// when
conversationRepository.updateLegalHoldStatus(CONVERSATION_ID, Conversation.LegalHoldStatus.ENABLED)
// then
verify(arrange.conversationDAO)
.suspendFunction(arrange.conversationDAO::updateLegalHoldStatusChangeNotified)
.with(eq(CONVERSATION_ID.toDao()), any())
.wasNotInvoked()
}

@Test
fun givenConversationId_whenObservingLegalHoldStatus_thenInvokeObserveLegalHoldStatusOnce() = runTest {
val (arrange, conversationRepository) = Arrangement()
.withObserveLegalHoldStatus()
.arrange()

conversationRepository.observeLegalHoldForConversation(CONVERSATION_ID)
conversationRepository.observeLegalHoldStatus(CONVERSATION_ID)

verify(arrange.conversationDAO)
.suspendFunction(arrange.conversationDAO::observeLegalHoldStatus)
.with(eq(CONVERSATION_ID.toDao()))
.wasInvoked(exactly = once)
}

@Test
fun givenConversationId_whenObservingLegalHoldStatusChangeNotified_thenInvokeObserveLegalHoldStatusChangeNotifiedOnce() = runTest {
val (arrange, conversationRepository) = Arrangement()
.withObserveLegalHoldStatusChangeNotified()
.arrange()

conversationRepository.observeLegalHoldStatusChangeNotified(CONVERSATION_ID)

verify(arrange.conversationDAO)
.suspendFunction(arrange.conversationDAO::observeLegalHoldForConversation)
.suspendFunction(arrange.conversationDAO::observeLegalHoldStatusChangeNotified)
.with(eq(CONVERSATION_ID.toDao()))
.wasInvoked(exactly = once)
}
Expand Down Expand Up @@ -1713,11 +1762,32 @@ class ConversationRepositoryTest {

fun withObserveLegalHoldStatus() = apply {
given(conversationDAO)
.suspendFunction(conversationDAO::observeLegalHoldForConversation)
.suspendFunction(conversationDAO::observeLegalHoldStatus)
.whenInvokedWith(any())
.thenReturn(flowOf(ConversationEntity.LegalHoldStatus.ENABLED))
}

fun withObserveLegalHoldStatusChangeNotified() = apply {
given(conversationDAO)
.suspendFunction(conversationDAO::observeLegalHoldStatusChangeNotified)
.whenInvokedWith(any())
.thenReturn(flowOf(true))
}

fun withUpdateLegalHoldStatus(updated: Boolean) = apply {
given(conversationDAO)
.suspendFunction(conversationDAO::updateLegalHoldStatus)
.whenInvokedWith(any(), any())
.thenReturn(updated)
}

fun withUpdateLegalHoldStatusChangeNotified(updated: Boolean) = apply {
given(conversationDAO)
.suspendFunction(conversationDAO::updateLegalHoldStatusChangeNotified)
.whenInvokedWith(any(), any())
.thenReturn(updated)
}

fun arrange() = this to conversationRepository
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Wire
* Copyright (C) 2023 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.StorageFailure
import com.wire.kalium.logic.data.conversation.Conversation
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.map
import io.mockative.Mock
import io.mockative.any
import io.mockative.given
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 ObserveConversationUnderLegalHoldNotifiedUseCaseTest {

private fun testObserving(
given: Either<StorageFailure, Pair<Conversation.LegalHoldStatus, Boolean>>,
expected: Boolean
) = runTest {
// given
val conversationId = ConversationId("conversationId", "domain")
val (_, useCase) = Arrangement()
.withObserveLegalHoldStatusForConversation(given.map { it.first })
.withObserveLegalHoldStatusChangeNotifiedForConversation(given.map { it.second })
.arrange()
// when
val result = useCase.invoke(conversationId)
// then
assertEquals(expected, result.first())
}

@Test
fun givenFailure_whenObserving_thenReturnTrue() =
testObserving(Either.Left(StorageFailure.DataNotFound), true)
@Test
fun givenLegalHoldEnabledAndNotNotified_whenObserving_thenReturnFalse() =
testObserving(Either.Right(Conversation.LegalHoldStatus.ENABLED to false), false)
@Test
fun givenLegalHoldEnabledAndNotified_whenObserving_thenReturnTrue() =
testObserving(Either.Right(Conversation.LegalHoldStatus.ENABLED to true), true)
@Test
fun givenLegalHoldDisabledAndNotNotified_whenObserving_thenReturnFalse() =
testObserving(Either.Right(Conversation.LegalHoldStatus.DISABLED to false), true)
@Test
fun givenLegalHoldDisabledAndNotified_whenObserving_thenReturnTrue() =
testObserving(Either.Right(Conversation.LegalHoldStatus.DISABLED to true), true)

private class Arrangement() {
@Mock
val conversationRepository = mock(ConversationRepository::class)

private val useCase: ObserveConversationUnderLegalHoldNotifiedUseCase by lazy {
ObserveConversationUnderLegalHoldNotifiedUseCaseImpl(conversationRepository)
}

fun arrange() = this to useCase
fun withObserveLegalHoldStatusForConversation(
result: Either<StorageFailure, Conversation.LegalHoldStatus>
) = apply {
given(conversationRepository)
.suspendFunction(conversationRepository::observeLegalHoldStatus)
.whenInvokedWith(any())
.thenReturn(flowOf(result))
}
fun withObserveLegalHoldStatusChangeNotifiedForConversation(
result: Either<StorageFailure, Boolean>
) = apply {
given(conversationRepository)
.suspendFunction(conversationRepository::observeLegalHoldStatusChangeNotified)
.whenInvokedWith(any())
.thenReturn(flowOf(result))
}
}
}
Loading
Loading