Skip to content

Commit

Permalink
feat: notify user when sending first message in conversation on legal…
Browse files Browse the repository at this point in the history
… hold [WPB-4566] (#2315)

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

* remove unused import

* fix detekt

* create separate table for legal hold change notified flag

* upsert instead of update

* update after merge

* make use case implementation internal

* make use case implementation internal

* update after merge

* fixes after merge
  • Loading branch information
saleniuk authored Dec 20, 2023
1 parent 88dbf8d commit 467884d
Show file tree
Hide file tree
Showing 20 changed files with 498 additions and 38 deletions.
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

0 comments on commit 467884d

Please sign in to comment.