From d40175ef8645046ed661a1fb604fe48bcc7aea50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:13:30 +0100 Subject: [PATCH] fix: hide LH indicators on conversations when self user is under LH [WPB-6391] (#3637) --- .../wire/android/mapper/ConversationMapper.kt | 4 +- .../com/wire/android/mapper/MessageMapper.kt | 2 +- .../conversation/ConversationSheetState.kt | 6 +- .../messages/item/RegularMessageItem.kt | 2 +- .../ui/home/conversations/mock/Mock.kt | 22 +- .../ui/home/conversations/model/UIMessage.kt | 2 +- .../ConversationListViewModel.kt | 40 +++- .../common/ConversationItemFactory.kt | 2 +- .../common/ConversationTitle.kt | 6 +- .../conversationslist/common/UserLabel.kt | 4 +- .../model/ConversationItem.kt | 12 +- .../com/wire/android/framework/TestMessage.kt | 2 +- .../MessageComposerViewModelArrangement.kt | 2 +- .../ConversationListViewModelTest.kt | 210 ++++++++++++------ 14 files changed, 212 insertions(+), 104 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index fb7ca3929a0..951ca60ceed 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -49,7 +49,7 @@ fun ConversationDetailsWithEvents.toConversationItem( groupName = conversationDetails.conversation.name.orEmpty(), conversationId = conversationDetails.conversation.id, mutedStatus = conversationDetails.conversation.mutedStatus, - isLegalHold = conversationDetails.conversation.legalHoldStatus.showLegalHoldIndicator(), + showLegalHoldIndicator = conversationDetails.conversation.legalHoldStatus.showLegalHoldIndicator(), lastMessageContent = lastMessage.toUIPreview(unreadEventCount), badgeEventType = parseConversationEventType( mutedStatus = conversationDetails.conversation.mutedStatus, @@ -83,7 +83,7 @@ fun ConversationDetailsWithEvents.toConversationItem( ), conversationId = conversationDetails.conversation.id, mutedStatus = conversationDetails.conversation.mutedStatus, - isLegalHold = conversationDetails.conversation.legalHoldStatus.showLegalHoldIndicator(), + showLegalHoldIndicator = conversationDetails.conversation.legalHoldStatus.showLegalHoldIndicator(), lastMessageContent = lastMessage.toUIPreview(unreadEventCount), badgeEventType = parsePrivateConversationEventType( conversationDetails.otherUser.connectionStatus, diff --git a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt index 24bb43d7d2f..cb0680e6e33 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt @@ -148,7 +148,7 @@ class MessageMapper @Inject constructor( is SelfUser, null -> Membership.None }, connectionState = getConnectionState(sender), - isLegalHold = sender?.isUnderLegalHold == true, + showLegalHoldIndicator = sender?.isUnderLegalHold == true, messageTime = MessageTime(message.date), messageStatus = getMessageStatus(message), messageId = message.id, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt index 0e48a9ac9a7..0279e37367b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt @@ -78,7 +78,7 @@ fun rememberConversationSheetState( protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, - isUnderLegalHold = isLegalHold + isUnderLegalHold = showLegalHoldIndicator ) } } @@ -102,7 +102,7 @@ fun rememberConversationSheetState( protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, - isUnderLegalHold = isLegalHold + isUnderLegalHold = showLegalHoldIndicator ) } } @@ -122,7 +122,7 @@ fun rememberConversationSheetState( protocol = Conversation.ProtocolInfo.Proteus, mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, - isUnderLegalHold = isLegalHold + isUnderLegalHold = showLegalHoldIndicator ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index 42bbe66926d..98a29bffee7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -513,7 +513,7 @@ private fun MessageAuthorRow(messageHeader: MessageHeader) { startPadding = dimensions().spacing6x, isDeleted = isSenderDeleted ) - if (isLegalHold) { + if (showLegalHoldIndicator) { LegalHoldIndicator(modifier = Modifier.padding(start = dimensions().spacing6x)) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt index 7b0461a57bc..b995681aed8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt @@ -65,7 +65,7 @@ val mockMessageTime = MessageTime(Instant.fromEpochSeconds(MOCK_TIME_IN_SECONDS) val mockHeader = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.Guest, - isLegalHold = true, + showLegalHoldIndicator = true, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, @@ -307,7 +307,7 @@ fun mockAssetMessage(assetId: String = "asset1", messageId: String = "msg1") = U header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.Guest, - isLegalHold = true, + showLegalHoldIndicator = true, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, @@ -337,7 +337,7 @@ fun mockAssetAudioMessage(assetId: String = "asset1", messageId: String = "msg1" header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.Guest, - isLegalHold = true, + showLegalHoldIndicator = true, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, @@ -396,7 +396,7 @@ fun mockedImageUIMessage( header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.External, - isLegalHold = false, + showLegalHoldIndicator = false, messageTime = mockMessageTime, messageStatus = messageStatus, messageId = messageId, @@ -417,7 +417,7 @@ fun getMockedMessages(): List = listOf( header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.Guest, - isLegalHold = true, + showLegalHoldIndicator = true, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, @@ -447,7 +447,7 @@ fun getMockedMessages(): List = listOf( header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.Guest, - isLegalHold = true, + showLegalHoldIndicator = true, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Delivered, isDeleted = true, @@ -468,7 +468,7 @@ fun getMockedMessages(): List = listOf( header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.External, - isLegalHold = false, + showLegalHoldIndicator = false, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, @@ -490,7 +490,7 @@ fun getMockedMessages(): List = listOf( header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.External, - isLegalHold = false, + showLegalHoldIndicator = false, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, @@ -512,7 +512,7 @@ fun getMockedMessages(): List = listOf( header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.External, - isLegalHold = false, + showLegalHoldIndicator = false, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Delivered, @@ -543,7 +543,7 @@ fun getMockedMessages(): List = listOf( header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.External, - isLegalHold = false, + showLegalHoldIndicator = false, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, @@ -565,7 +565,7 @@ fun getMockedMessages(): List = listOf( header = MessageHeader( username = UIText.DynamicString("John Doe"), membership = Membership.External, - isLegalHold = false, + showLegalHoldIndicator = false, messageTime = mockMessageTime, messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index c3f70e439d8..bd507cdf380 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -127,7 +127,7 @@ sealed interface UIMessage { data class MessageHeader( val username: UIText, val membership: Membership, - val isLegalHold: Boolean, + val showLegalHoldIndicator: Boolean, val messageTime: MessageTime, val messageStatus: MessageStatus, val messageId: String, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index cbea3a7fcdd..2b5c4b66748 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.insertSeparators +import androidx.paging.map import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.di.CurrentAccount @@ -64,6 +65,8 @@ import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMet import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase +import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import com.wire.kalium.logic.feature.team.Result @@ -78,6 +81,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow @@ -121,6 +125,7 @@ class ConversationListViewModelPreview( @HiltViewModel(assistedFactory = ConversationListViewModelImpl.Factory::class) class ConversationListViewModelImpl @AssistedInject constructor( @Assisted val conversationsSource: ConversationsSource, + @Assisted private val usePagination: Boolean = BuildConfig.PAGINATED_CONVERSATION_LIST_ENABLED, dispatcher: DispatcherProvider, private val updateConversationMutedStatus: UpdateConversationMutedStatusUseCase, private val getConversationsPaginated: GetConversationsFromSearchUseCase, @@ -133,6 +138,7 @@ class ConversationListViewModelImpl @AssistedInject constructor( private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase, private val updateConversationArchivedStatus: UpdateConversationArchivedStatusUseCase, + private val observeLegalHoldStateForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, @CurrentAccount val currentAccount: UserId, private val wireSessionImageLoader: WireSessionImageLoader, private val userTypeMapper: UserTypeMapper, @@ -140,7 +146,10 @@ class ConversationListViewModelImpl @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(conversationsSource: ConversationsSource): ConversationListViewModelImpl + fun create( + conversationsSource: ConversationsSource, + usePagination: Boolean = BuildConfig.PAGINATED_CONVERSATION_LIST_ENABLED, + ): ConversationListViewModelImpl } private val _infoMessage = MutableSharedFlow() @@ -173,7 +182,11 @@ class ConversationListViewModelImpl @AssistedInject constructor( conversationFilter = conversationsSource.toFilter(), onlyInteractionEnabled = false, newActivitiesOnTop = containsNewActivitiesSection, - ).map { + ).combine(observeLegalHoldStateForSelfUser()) { conversations, selfUserLegalHoldStatus -> + conversations.map { + it.hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus) + } + }.map { it.insertSeparators { before, after -> when { // do not add separators if the list shouldn't show conversations grouped into different folders @@ -200,7 +213,7 @@ class ConversationListViewModelImpl @AssistedInject constructor( private var notPaginatedConversationListState by mutableStateOf(ConversationListState.NotPaginated()) override val conversationListState: ConversationListState - get() = if (BuildConfig.PAGINATED_CONVERSATION_LIST_ENABLED) { + get() = if (usePagination) { ConversationListState.Paginated( conversations = conversationsPaginatedFlow, domain = currentAccount.domain @@ -210,7 +223,7 @@ class ConversationListViewModelImpl @AssistedInject constructor( } init { - if (!BuildConfig.PAGINATED_CONVERSATION_LIST_ENABLED) { + if (!usePagination) { viewModelScope.launch { searchQueryFlow .debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE } @@ -220,13 +233,13 @@ class ConversationListViewModelImpl @AssistedInject constructor( observeConversationListDetailsWithEvents( fromArchive = conversationsSource == ConversationsSource.ARCHIVE, conversationFilter = conversationsSource.toFilter() - ).map { - it.map { conversationDetails -> + ).combine(observeLegalHoldStateForSelfUser()) { conversations, selfUserLegalHoldStatus -> + conversations.map { conversationDetails -> conversationDetails.toConversationItem( wireSessionImageLoader = wireSessionImageLoader, userTypeMapper = userTypeMapper, searchQuery = searchQuery, - ) + ).hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus) } to searchQuery } } @@ -438,6 +451,19 @@ private fun ConversationsSource.toFilter(): ConversationFilter = when (this) { ConversationsSource.ONE_ON_ONE -> ConversationFilter.ONE_ON_ONE } +private fun ConversationItem.hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus: LegalHoldStateForSelfUser) = + // if self user is under legal hold then we shouldn't show legal hold indicator next to every conversation + // the indication is shown in the header of the conversation list for self user in that case and it's enough + when (selfUserLegalHoldStatus) { + is LegalHoldStateForSelfUser.Enabled -> when (this) { + is ConversationItem.ConnectionConversation -> this.copy(showLegalHoldIndicator = false) + is ConversationItem.GroupConversation -> this.copy(showLegalHoldIndicator = false) + is ConversationItem.PrivateConversation -> this.copy(showLegalHoldIndicator = false) + } + + else -> this + } + @Suppress("ComplexMethod") private fun List.withFolders(source: ConversationsSource): Map> { return when (source) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt index 44ac89e3f7a..9a8f216b91b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt @@ -159,7 +159,7 @@ private fun GeneralConversationItem( title = { ConversationTitle( name = groupName.ifEmpty { stringResource(id = R.string.member_name_deleted_label) }, - isLegalHold = conversation.isLegalHold, + showLegalHoldIndicator = conversation.showLegalHoldIndicator, searchQuery = searchQuery ) }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationTitle.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationTitle.kt index 0382e16960e..1bbf7362869 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationTitle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationTitle.kt @@ -38,7 +38,7 @@ fun ConversationTitle( name: String, searchQuery: String, modifier: Modifier = Modifier, - isLegalHold: Boolean = false, + showLegalHoldIndicator: Boolean = false, badges: @Composable () -> Unit = {} ) { Row( @@ -57,7 +57,7 @@ fun ConversationTitle( HighlightName(name = name, searchQuery = searchQuery) } badges() - if (isLegalHold) { + if (showLegalHoldIndicator) { Spacer(modifier = Modifier.width(6.dp)) LegalHoldIndicator() } @@ -67,5 +67,5 @@ fun ConversationTitle( @Preview(widthDp = 200) @Composable fun PreviewConversationTitle() { - ConversationTitle("very very loooooooooooong name", searchQuery = "test", isLegalHold = true) + ConversationTitle("very very loooooooooooong name", searchQuery = "test", showLegalHoldIndicator = true) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/UserLabel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/UserLabel.kt index 0daf7203d99..c7ec40a8d7d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/UserLabel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/UserLabel.kt @@ -39,7 +39,7 @@ fun UserLabel( with(userInfoLabel) { ConversationTitle( name = if (unavailable) stringResource(id = R.string.username_unavailable_label) else labelName, - isLegalHold = isLegalHold, + showLegalHoldIndicator = showLegalHoldIndicator, modifier = modifier, badges = { if (membership.hasLabel()) { @@ -54,7 +54,7 @@ fun UserLabel( data class UserInfoLabel( val labelName: String, - val isLegalHold: Boolean, + val showLegalHoldIndicator: Boolean, val membership: Membership, val unavailable: Boolean = false, val proteusVerificationStatus: Conversation.VerificationStatus? = null, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt index 467805827d1..dbc5f36305c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt @@ -33,7 +33,7 @@ import com.wire.kalium.logic.data.user.type.isTeammate sealed class ConversationItem : ConversationFolderItem { abstract val conversationId: ConversationId abstract val mutedStatus: MutedConversationStatus - abstract val isLegalHold: Boolean + abstract val showLegalHoldIndicator: Boolean abstract val lastMessageContent: UILastMessageContent? abstract val badgeEventType: BadgeEventType abstract val teamId: TeamId? @@ -53,7 +53,7 @@ sealed class ConversationItem : ConversationFolderItem { val isSelfUserMember: Boolean = true, override val conversationId: ConversationId, override val mutedStatus: MutedConversationStatus, - override val isLegalHold: Boolean = false, + override val showLegalHoldIndicator: Boolean = false, override val lastMessageContent: UILastMessageContent?, override val badgeEventType: BadgeEventType, override val teamId: TeamId?, @@ -71,7 +71,7 @@ sealed class ConversationItem : ConversationFolderItem { val blockingState: BlockingState, override val conversationId: ConversationId, override val mutedStatus: MutedConversationStatus, - override val isLegalHold: Boolean = false, + override val showLegalHoldIndicator: Boolean = false, override val lastMessageContent: UILastMessageContent?, override val badgeEventType: BadgeEventType, override val teamId: TeamId?, @@ -87,7 +87,7 @@ sealed class ConversationItem : ConversationFolderItem { val conversationInfo: ConversationInfo, override val conversationId: ConversationId, override val mutedStatus: MutedConversationStatus, - override val isLegalHold: Boolean = false, + override val showLegalHoldIndicator: Boolean = false, override val lastMessageContent: UILastMessageContent?, override val badgeEventType: BadgeEventType, override val isArchived: Boolean = false, @@ -123,7 +123,7 @@ val OtherUser.BlockState: BlockingState fun ConversationItem.PrivateConversation.toUserInfoLabel() = UserInfoLabel( labelName = conversationInfo.name, - isLegalHold = isLegalHold, + showLegalHoldIndicator = showLegalHoldIndicator, membership = conversationInfo.membership, unavailable = conversationInfo.isSenderUnavailable, mlsVerificationStatus = mlsVerificationStatus, @@ -133,7 +133,7 @@ fun ConversationItem.PrivateConversation.toUserInfoLabel() = fun ConversationItem.ConnectionConversation.toUserInfoLabel() = UserInfoLabel( labelName = conversationInfo.name, - isLegalHold = isLegalHold, + showLegalHoldIndicator = showLegalHoldIndicator, membership = conversationInfo.membership, unavailable = conversationInfo.isSenderUnavailable ) diff --git a/app/src/test/kotlin/com/wire/android/framework/TestMessage.kt b/app/src/test/kotlin/com/wire/android/framework/TestMessage.kt index f5dbab4b6e5..17cac8553cf 100644 --- a/app/src/test/kotlin/com/wire/android/framework/TestMessage.kt +++ b/app/src/test/kotlin/com/wire/android/framework/TestMessage.kt @@ -145,7 +145,7 @@ object TestMessage { val UI_MESSAGE_HEADER = MessageHeader( username = UIText.DynamicString("username"), membership = Membership.Guest, - isLegalHold = true, + showLegalHoldIndicator = true, messageTime = MessageTime(Instant.parse("2022-03-30T15:36:00.000Z")), messageStatus = MessageStatus( flowStatus = MessageFlowStatus.Sent, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt index a0413084b0e..4387a91ec30 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt @@ -225,7 +225,7 @@ internal fun mockUITextMessage(id: String = "someId", userName: String = "mockUs every { it.header } returns mockk().also { every { it.messageId } returns id every { it.username } returns UIText.DynamicString(userName) - every { it.isLegalHold } returns false + every { it.showLegalHoldIndicator } returns false every { it.messageTime } returns MessageTime(Instant.DISTANT_PAST) every { it.messageStatus } returns MessageStatus( flowStatus = MessageFlowStatus.Sent, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt index d9e1cdb253f..5f6ea2533dd 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt @@ -19,7 +19,11 @@ package com.wire.android.ui.home.conversationslist +import androidx.paging.LoadState +import androidx.paging.LoadStates import androidx.paging.PagingData +import androidx.paging.testing.asSnapshot +import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri @@ -29,6 +33,7 @@ import com.wire.android.framework.TestUser import com.wire.android.mapper.UserTypeMapper import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase +import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents @@ -47,6 +52,8 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetails import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase +import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import io.mockk.MockKAnnotations @@ -55,7 +62,9 @@ import io.mockk.coVerify import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -66,64 +75,116 @@ class ConversationListViewModelTest { private val dispatcherProvider = TestDispatcherProvider() - // TODO: reenable this test once pagination is implemented -// @Test -// fun `given initial empty search query, when collecting conversations, then call use case with proper params`() = -// runTest(dispatcherProvider.main()) { -// // Given -// val (arrangement, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.MAIN).arrange() -// -// // When -// conversationListViewModel.conversationListState.foldersWithConversations.test { -// // Then -// coVerify(exactly = 1) { -// arrangement.getConversationsPaginated("", false, true, false) -// } -// cancelAndIgnoreRemainingEvents() -// } -// } - - // TODO: reenable this test once pagination is implemented -// @Test -// fun `given updated non-empty search query, when collecting conversations, then call use case with proper params`() = -// runTest(dispatcherProvider.main()) { -// // Given -// val searchQueryText = "search" -// val (arrangement, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.MAIN).arrange() -// -// // When -// conversationListViewModel.conversationListState.foldersWithConversations.test { -// conversationListViewModel.searchQueryChanged(searchQueryText) -// advanceUntilIdle() -// -// // Then -// coVerify(exactly = 1) { -// arrangement.getConversationsPaginated(searchQueryText, false, true, false) -// } -// cancelAndIgnoreRemainingEvents() -// } -// } - - // TODO: reenable this test once pagination is implemented -// @Test -// fun `given updated non-empty search query, when collecting archived, then call use case with proper params`() = -// runTest(dispatcherProvider.main()) { -// // Given -// val searchQueryText = "search" -// val (arrangement, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.ARCHIVE).arrange() -// -// // When -// conversationListViewModel.conversationListState.foldersWithConversations.test { -// conversationListViewModel.searchQueryChanged(searchQueryText) -// advanceUntilIdle() -// -// // Then -// coVerify(exactly = 1) { -// arrangement.getConversationsPaginated(searchQueryText, true, false, false) -// } -// cancelAndIgnoreRemainingEvents() -// } -// } + @Test + fun `given initial empty search query, when collecting conversations, then call use case with proper params`() = + runTest(dispatcherProvider.main()) { + // Given + val (arrangement, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.MAIN).arrange() + + // When + (conversationListViewModel.conversationListState as ConversationListState.Paginated).conversations.test { + // Then + coVerify(exactly = 1) { + arrangement.getConversationsPaginated("", false, true, false) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given updated non-empty search query, when collecting conversations, then call use case with proper params`() = + runTest(dispatcherProvider.main()) { + // Given + val searchQueryText = "search" + val (arrangement, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.MAIN).arrange() + + // When + (conversationListViewModel.conversationListState as ConversationListState.Paginated).conversations.test { + conversationListViewModel.searchQueryChanged(searchQueryText) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { + arrangement.getConversationsPaginated(searchQueryText, false, true, false) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given updated non-empty search query, when collecting archived, then call use case with proper params`() = + runTest(dispatcherProvider.main()) { + // Given + val searchQueryText = "search" + val (arrangement, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.ARCHIVE).arrange() + + // When + (conversationListViewModel.conversationListState as ConversationListState.Paginated).conversations.test { + conversationListViewModel.searchQueryChanged(searchQueryText) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { + arrangement.getConversationsPaginated(searchQueryText, true, false, false) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given self user is under legal hold, when collecting conversations, then hide LH indicators`() = + runTest(dispatcherProvider.main()) { + // Given + val conversations = listOf( + TestConversationItem.CONNECTION.copy(conversationId = ConversationId("conn_1", ""), showLegalHoldIndicator = true), + TestConversationItem.CONNECTION.copy(conversationId = ConversationId("conn_2", ""), showLegalHoldIndicator = false), + TestConversationItem.PRIVATE.copy(conversationId = ConversationId("private_1", ""), showLegalHoldIndicator = true), + TestConversationItem.PRIVATE.copy(conversationId = ConversationId("private_2", ""), showLegalHoldIndicator = false), + TestConversationItem.GROUP.copy(conversationId = ConversationId("group_1", ""), showLegalHoldIndicator = true), + TestConversationItem.GROUP.copy(conversationId = ConversationId("group_2", ""), showLegalHoldIndicator = false), + ).associateBy { it.conversationId } + val (_, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.MAIN) + .withConversationsPaginated(conversations.values.toList()) + .withSelfUserLegalHoldState(LegalHoldStateForSelfUser.Enabled) + .arrange() + advanceUntilIdle() + + // When + (conversationListViewModel.conversationListState as ConversationListState.Paginated).conversations.asSnapshot() + .filterIsInstance() + .forEach { + // Then + assertEquals(false, it.showLegalHoldIndicator) // self user is under LH so hide LH indicators next to conversations + } + } + + @Test + fun `given self user is not under legal hold, when collecting conversations, then show LH indicator when conversation is under LH`() = + runTest(dispatcherProvider.main()) { + // Given + val conversations = listOf( + TestConversationItem.CONNECTION.copy(conversationId = ConversationId("conn_1", ""), showLegalHoldIndicator = true), + TestConversationItem.CONNECTION.copy(conversationId = ConversationId("conn_2", ""), showLegalHoldIndicator = false), + TestConversationItem.PRIVATE.copy(conversationId = ConversationId("private_1", ""), showLegalHoldIndicator = true), + TestConversationItem.PRIVATE.copy(conversationId = ConversationId("private_2", ""), showLegalHoldIndicator = false), + TestConversationItem.GROUP.copy(conversationId = ConversationId("group_1", ""), showLegalHoldIndicator = true), + TestConversationItem.GROUP.copy(conversationId = ConversationId("group_2", ""), showLegalHoldIndicator = false), + ).associateBy { it.conversationId } + val (_, conversationListViewModel) = Arrangement(conversationsSource = ConversationsSource.MAIN) + .withConversationsPaginated(conversations.values.toList()) + .withSelfUserLegalHoldState(LegalHoldStateForSelfUser.Disabled) + .arrange() + advanceUntilIdle() + + // When + (conversationListViewModel.conversationListState as ConversationListState.Paginated).conversations.asSnapshot() + .filterIsInstance() + .forEach { + // Then + val expected = conversations[it.conversationId]!!.showLegalHoldIndicator // show indicator when conversation is under LH + assertEquals(expected, it.showLegalHoldIndicator) + } + } @Test fun `given a valid conversation muting state, when calling muteConversation, then should call with call the UseCase`() = @@ -207,16 +268,16 @@ class ConversationListViewModelTest { private lateinit var observeConversationListDetailsWithEventsUseCase: ObserveConversationListDetailsWithEventsUseCase + @MockK + private lateinit var observeLegalHoldStateForSelfUserUseCase: ObserveLegalHoldStateForSelfUserUseCase + @MockK private lateinit var wireSessionImageLoader: WireSessionImageLoader init { MockKAnnotations.init(this, relaxUnitFun = true) - coEvery { - getConversationsPaginated.invoke(any(), any(), any(), any()) - } returns flowOf( - PagingData.from(listOf(TestConversationItem.CONNECTION, TestConversationItem.PRIVATE, TestConversationItem.GROUP)) - ) + withConversationsPaginated(listOf(TestConversationItem.CONNECTION, TestConversationItem.PRIVATE, TestConversationItem.GROUP)) + withSelfUserLegalHoldState(LegalHoldStateForSelfUser.Disabled) coEvery { observeConversationListDetailsWithEventsUseCase.invoke(false, ConversationFilter.ALL) } returns flowOf( listOf( TestConversationDetails.CONNECTION, @@ -245,6 +306,25 @@ class ConversationListViewModelTest { coEvery { unblockUser(any()) } returns UnblockUserResult.Success } + fun withConversationsPaginated(items: List) = apply { + coEvery { + getConversationsPaginated.invoke(any(), any(), any(), any()) + } returns flowOf( + PagingData.from( + data = items, + sourceLoadStates = LoadStates( + prepend = LoadState.NotLoading(true), + append = LoadState.NotLoading(true), + refresh = LoadState.NotLoading(true), + ), + ) + ) + } + + fun withSelfUserLegalHoldState(LegalHoldStateForSelfUser: LegalHoldStateForSelfUser) = apply { + coEvery { observeLegalHoldStateForSelfUserUseCase() } returns flowOf(LegalHoldStateForSelfUser) + } + fun arrange() = this to ConversationListViewModelImpl( conversationsSource = conversationsSource, dispatcher = dispatcherProvider, @@ -260,8 +340,10 @@ class ConversationListViewModelTest { updateConversationArchivedStatus = updateConversationArchivedStatus, currentAccount = TestUser.SELF_USER_ID, observeConversationListDetailsWithEvents = observeConversationListDetailsWithEventsUseCase, + observeLegalHoldStateForSelfUser = observeLegalHoldStateForSelfUserUseCase, userTypeMapper = UserTypeMapper(), - wireSessionImageLoader = wireSessionImageLoader + wireSessionImageLoader = wireSessionImageLoader, + usePagination = true, ) }