diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt index 0b3107ade80..eccb6dc2598 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt @@ -45,6 +45,7 @@ import com.wire.kalium.logic.feature.connection.BlockUserResult import com.wire.kalium.logic.feature.connection.BlockUserUseCase import com.wire.kalium.logic.feature.connection.UnblockUserResult import com.wire.kalium.logic.feature.connection.UnblockUserUseCase +import com.wire.kalium.logic.feature.conversation.AdminlessConversationFailure import com.wire.kalium.logic.feature.conversation.ArchiveStatusUpdateResult import com.wire.kalium.logic.feature.conversation.CheckConversationLeaveConditionsUseCase import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase @@ -79,6 +80,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap + @ViewModelScopedPreview interface ConversationOptionsMenuViewModel : ActionsManager { val leaveGroupDialogState: VisibilityState get() = VisibilityState() @@ -287,7 +289,14 @@ class ConversationOptionsMenuViewModelImpl( leaveConversation(conversationId) }.let { result -> when (result) { - is RemoveMemberFromConversationUseCase.Result.Failure -> onMessage(HomeSnackBarMessage.LeaveConversationError) + is RemoveMemberFromConversationUseCase.Result.Failure -> { + when (val cause = result.cause) { + is AdminlessConversationFailure -> + sendAction(ConversationOptionsMenuViewAction.PromoteAdmin(conversationId, cause.eligibleMembers)) + else -> onMessage(HomeSnackBarMessage.LeaveConversationError) + } + } + is RemoveMemberFromConversationUseCase.Result.Success -> when { shouldDelete -> when (markAsDeletedLocallyAndEnqueueWorkerToDeleteCompletely(conversationId)) { false -> onMessage(HomeSnackBarMessage.LeaveConversationError) @@ -434,6 +443,7 @@ sealed interface ConversationOptionsMenuState { sealed interface ConversationOptionsMenuViewAction { data class Message(val message: SnackBarMessage) : ConversationOptionsMenuViewAction + data class PromoteAdmin(val conversationId: ConversationId, val eligibleMembers: List) : ConversationOptionsMenuViewAction data class Left(val conversationId: ConversationId, val conversationName: String) : ConversationOptionsMenuViewAction data class Deleted(val conversationId: ConversationId, val conversationName: String) : ConversationOptionsMenuViewAction data class DeletedLocally(val conversationId: ConversationId, val conversationName: String) : ConversationOptionsMenuViewAction diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt index 5d6a9bb5125..c55d0c2effc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt @@ -46,6 +46,7 @@ import com.wire.android.ui.home.conversationslist.model.DeleteGroupDialogState import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.UserId @SuppressLint("ComposeModifierMissing") @Composable @@ -55,7 +56,7 @@ fun ConversationOptionsModalSheetLayout( onLeftConversation: () -> Unit = {}, onDeletedConversation: () -> Unit = {}, onDeletedConversationLocally: () -> Unit = {}, - onPromoteAdmin: (ConversationId) -> Unit = {}, + onPromoteAdmin: (ConversationId, List) -> Unit = { _, _ -> }, openConversationDebugMenu: (ConversationId) -> Unit = {}, viewModel: ConversationOptionsMenuViewModel = conversationOptionsMenuViewModel() @@ -70,7 +71,7 @@ fun ConversationOptionsModalSheetLayout( dialogState = viewModel.leaveGroupOptionsDialogState, onPromoteAdmin = { state -> viewModel.leaveGroupOptionsDialogState.dismiss() - onPromoteAdmin(state.conversationId) + onPromoteAdmin(state.conversationId, emptyList()) }, onDeleteGroup = { state -> viewModel.leaveGroupOptionsDialogState.dismiss() @@ -106,6 +107,7 @@ fun ConversationOptionsModalSheetLayout( is ConversationOptionsMenuViewAction.Deleted -> onDeletedConversation() is ConversationOptionsMenuViewAction.DeletedLocally -> onDeletedConversationLocally() is ConversationOptionsMenuViewAction.Left -> onLeftConversation() + is ConversationOptionsMenuViewAction.PromoteAdmin -> onPromoteAdmin(action.conversationId, action.eligibleMembers) is ConversationOptionsMenuViewAction.Message -> sheetState.hide { snackbarHostState.showSnackbar(action.message.uiText.asString(context.resources)) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationSearchFolderViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationSearchFolderViewModelFactory.kt index 16341c08c77..4b2783033af 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationSearchFolderViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationSearchFolderViewModelFactory.kt @@ -29,6 +29,7 @@ import com.wire.android.ui.home.conversations.search.messages.SearchConversation import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCase @@ -46,6 +47,7 @@ class ConversationSearchFolderViewModelFactory @Inject constructor( private val getConversationMessagesFromSearch: GetConversationMessagesFromSearchUseCase, private val promoteAdminAndLeave: PromoteAdminAndLeaveConversationUseCase, private val observeEligibleMembers: ObserveEligibleMembersForConversationAdminRoleUseCase, + private val observeConversationMembers: ObserveConversationMembersUseCase, ) { fun conversationFoldersViewModel(args: ConversationFoldersStateArgs) = ConversationFoldersVMImpl( args = args, @@ -78,6 +80,7 @@ class ConversationSearchFolderViewModelFactory @Inject constructor( fun promoteAdminViewModel(savedStateHandle: SavedStateHandle) = PromoteAdminViewModel( promoteAdminAndLeave = promoteAdminAndLeave, observeEligibleMembers = observeEligibleMembers, + observeConversationMembers = observeConversationMembers, dispatchers = dispatchers, savedStateHandle = savedStateHandle, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt index d01824ddaac..c57e14dd3b0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt @@ -107,6 +107,7 @@ import com.ramcosta.composedestinations.generated.app.destinations.UpdateAppsAcc import com.ramcosta.composedestinations.generated.app.destinations.PromoteAdminScreenDestination import com.wire.android.ui.home.conversations.details.editguestaccess.EditGuestAccessParams import com.wire.android.ui.home.conversations.promoteadmin.PromoteAdminNavArgs +import com.wire.android.ui.home.conversations.promoteadmin.toPromoteAdminEligibleMemberArgs import com.wire.android.ui.home.conversations.details.options.GroupConversationOptions import com.wire.android.ui.home.conversations.details.options.GroupConversationOptionsState import com.wire.android.ui.home.conversations.details.options.LoadingGroupConversation @@ -131,6 +132,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.UserId import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -304,8 +306,14 @@ fun GroupConversationDetailsScreen( ) ) }, - onPromoteAdmin = { conversationId -> - navigator.navigate(NavigationCommand(PromoteAdminScreenDestination(PromoteAdminNavArgs(conversationId)))) + onPromoteAdmin = { conversationId, eligibleMembers -> + navigator.navigate( + NavigationCommand( + PromoteAdminScreenDestination( + PromoteAdminNavArgs(conversationId, eligibleMembers.toPromoteAdminEligibleMemberArgs()) + ) + ) + ) }, openConversationDebugMenu = { navigator.navigate( @@ -384,7 +392,7 @@ private fun GroupConversationDetailsContent( onMoveToFolder: (ConversationFoldersNavArgs) -> Unit = {}, onLeftConversation: () -> Unit = {}, onDeletedConversation: () -> Unit = {}, - onPromoteAdmin: (ConversationId) -> Unit = {}, + onPromoteAdmin: (ConversationId, List) -> Unit = { _, _ -> }, openConversationDebugMenu: (ConversationId) -> Unit = {}, initialPageIndex: GroupConversationDetailsTabItem = GroupConversationDetailsTabItem.OPTIONS, isScreenLoading: StateFlow = MutableStateFlow(false), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminNavArgs.kt index b6248a22053..210e48df1c9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminNavArgs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminNavArgs.kt @@ -20,9 +20,19 @@ package com.wire.android.ui.home.conversations.promoteadmin import android.os.Parcelable import com.wire.android.ui.home.conversations.QualifiedIdParceler import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.user.UserId import kotlinx.parcelize.Parcelize import kotlinx.parcelize.TypeParceler @Parcelize -@TypeParceler() -data class PromoteAdminNavArgs(val conversationId: ConversationId) : Parcelable +@TypeParceler() +data class PromoteAdminNavArgs( + val conversationId: ConversationId, + val eligibleMembers: ArrayList = arrayListOf(), +) : Parcelable + +fun List.toPromoteAdminEligibleMemberArgs(): ArrayList = + mapTo(ArrayList()) { "${it.value}$USER_ID_DOMAIN_SEPARATOR${it.domain}" } + +const val USER_ID_DOMAIN_SEPARATOR = "@" diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModel.kt index 22103707252..935fc7a3d16 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModel.kt @@ -27,6 +27,7 @@ import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.conversation.MemberDetails import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase import kotlinx.coroutines.NonCancellable @@ -40,13 +41,16 @@ import kotlinx.coroutines.withContext class PromoteAdminViewModel( private val promoteAdminAndLeave: PromoteAdminAndLeaveConversationUseCase, private val observeEligibleMembers: ObserveEligibleMembersForConversationAdminRoleUseCase, + private val observeConversationMembers: ObserveConversationMembersUseCase, private val dispatchers: DispatcherProvider, savedStateHandle: SavedStateHandle, ) : ActionsViewModel() { private val navArgs: PromoteAdminNavArgs = savedStateHandle.navArgs() - private val allMembers = MutableStateFlow>(emptyList()) + private val clientEligibleMembers = MutableStateFlow>(emptyList()) + private val allConversationMembers = MutableStateFlow>(emptyList()) + private val eligibleMemberIds = MutableStateFlow(navArgs.eligibleMembers.map { it.toUserId() }.toSet()) private val searchQuery = MutableStateFlow("") private val selectedUserId = MutableStateFlow(null) @@ -55,7 +59,18 @@ class PromoteAdminViewModel( init { viewModelScope.launch { - combine(allMembers, searchQuery, selectedUserId) { members, query, selected -> + combine( + clientEligibleMembers, + allConversationMembers, + eligibleMemberIds, + searchQuery, + selectedUserId + ) { clientEligibleMembers, allConversationMembers, eligibleIds, query, selected -> + val members = if (eligibleIds.isEmpty()) { + clientEligibleMembers + } else { + allConversationMembers.filterByEligibleIds(eligibleIds) + } PromoteAdminState( searchQuery = query, filteredMembers = filter(members, query), @@ -69,7 +84,13 @@ class PromoteAdminViewModel( viewModelScope.launch { observeEligibleMembers(navArgs.conversationId).collect { members -> - allMembers.value = members.map { it.toMemberItem() } + clientEligibleMembers.value = members.map { it.toMemberItem() } + } + } + + viewModelScope.launch { + observeConversationMembers(navArgs.conversationId).collect { members -> + allConversationMembers.value = members.map { it.toMemberItem() } } } } @@ -92,9 +113,17 @@ class PromoteAdminViewModel( } val action = when (result) { - PromoteAdminAndLeaveConversationUseCase.Result.Success -> PromoteAdminAction.Success - PromoteAdminAndLeaveConversationUseCase.Result.FailedToPromoteUser -> PromoteAdminAction.FailedToPromoteUser - PromoteAdminAndLeaveConversationUseCase.Result.FailedToLeaveConversation -> PromoteAdminAction.FailedToLeaveConversation + is PromoteAdminAndLeaveConversationUseCase.Result.Success -> PromoteAdminAction.Success + is PromoteAdminAndLeaveConversationUseCase.Result.FailedToPromoteUser -> PromoteAdminAction.FailedToPromoteUser + is PromoteAdminAndLeaveConversationUseCase.Result.FailedToLeaveConversation -> { + if (result.eligibleMembers.isEmpty()) { + PromoteAdminAction.FailedToLeaveConversation + } else { + selectedUserId.value = null + eligibleMemberIds.value = result.eligibleMembers.toSet() + PromoteAdminAction.FailedToLeaveConversation + } + } } sendAction(action) @@ -114,6 +143,14 @@ class PromoteAdminViewModel( } } + private fun List.filterByEligibleIds(eligibleIds: Set): List = + if (eligibleIds.isEmpty()) this else filter { it.userId in eligibleIds } + + private fun String.toUserId() = UserId( + value = substringBeforeLast(USER_ID_DOMAIN_SEPARATOR), + domain = substringAfterLast(USER_ID_DOMAIN_SEPARATOR), + ) + private fun MemberDetails.toMemberItem() = PromoteAdminMemberItem( userId = user.id, name = user.name.orEmpty(), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt index 2d97683210a..1d643480a22 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt @@ -66,6 +66,7 @@ import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.debug.conversation.DebugConversationScreenNavArgs import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState import com.wire.android.ui.home.conversations.promoteadmin.PromoteAdminNavArgs +import com.wire.android.ui.home.conversations.promoteadmin.toPromoteAdminEligibleMemberArgs import com.wire.android.ui.home.conversationListViewModel import com.wire.android.ui.home.conversationslist.common.ConversationList import com.wire.android.ui.home.conversationslist.model.ConversationItem @@ -276,7 +277,15 @@ fun ConversationsScreenContent( ConversationOptionsModalSheetLayout( sheetState = sheetState, openConversationFolders = { navigator.navigate(NavigationCommand(ConversationFoldersScreenDestination(it))) }, - onPromoteAdmin = { navigator.navigate(NavigationCommand(PromoteAdminScreenDestination(PromoteAdminNavArgs(it)))) }, + onPromoteAdmin = { conversationId, eligibleMembers -> + navigator.navigate( + NavigationCommand( + PromoteAdminScreenDestination( + PromoteAdminNavArgs(conversationId, eligibleMembers.toPromoteAdminEligibleMemberArgs()) + ) + ) + ) + }, openConversationDebugMenu = { conversationId -> navigator.navigate( NavigationCommand( diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index db6b84886c2..c1661b673d7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -201,8 +201,8 @@ class OtherUserProfileScreenViewModel @Inject constructor( ) } when (response) { - is RemoveMemberFromConversationUseCase.Result.Failure -> onMessage(RemoveConversationMemberError) is RemoveMemberFromConversationUseCase.Result.Success -> onMessage(RemoveConversationMemberSuccess(state.userName)) + is RemoveMemberFromConversationUseCase.Result.Failure -> onMessage(RemoveConversationMemberError) } removeConversationMemberDialogState.dismiss() } diff --git a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModelTest.kt index f813ccfb455..d3c049f93f2 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModelTest.kt @@ -36,6 +36,7 @@ import com.wire.kalium.logic.feature.connection.BlockUserResult import com.wire.kalium.logic.feature.connection.BlockUserUseCase import com.wire.kalium.logic.feature.connection.UnblockUserResult import com.wire.kalium.logic.feature.connection.UnblockUserUseCase +import com.wire.kalium.logic.feature.conversation.AdminlessConversationFailure import com.wire.kalium.logic.feature.conversation.ArchiveStatusUpdateResult import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase import com.wire.kalium.logic.feature.conversation.ConversationUpdateStatusResult @@ -372,6 +373,46 @@ class ConversationOptionsMenuViewModelTest { } } + @Test + fun `given adminless failure with eligible members, when leaving group, then navigate to promote admin`() = + runTest(dispatcherProvider.main()) { + val eligibleMembers = listOf(userId) + val (arrangement, viewModel) = Arrangement() + .withLeaveConversation( + RemoveMemberFromConversationUseCase.Result.Failure(AdminlessConversationFailure(eligibleMembers)) + ) + .arrange() + + viewModel.actions.test { + viewModel.leaveGroup(conversationId, "name", false) + + coVerify(exactly = 1) { arrangement.leaveConversation(conversationId) } + assertIs(awaitItem()).also { + assertEquals(conversationId, it.conversationId) + assertEquals(eligibleMembers, it.eligibleMembers) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given adminless failure without eligible members, when leaving group, then show leave error`() = + runTest(dispatcherProvider.main()) { + val (arrangement, viewModel) = Arrangement() + .withLeaveConversation(RemoveMemberFromConversationUseCase.Result.Failure(CoreFailure.Unknown(null))) + .arrange() + + viewModel.actions.test { + viewModel.leaveGroup(conversationId, "name", false) + + coVerify(exactly = 1) { arrangement.leaveConversation(conversationId) } + assertIs(awaitItem()).also { + assertIs(it.message) + } + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `given success, when leaving group with deleting, then call proper action`() = runTest(dispatcherProvider.main()) { val (arrangement, viewModel) = Arrangement() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelTest.kt index 5551ac93802..abaf71289ec 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelTest.kt @@ -28,6 +28,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.MemberDetails import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase import io.mockk.MockKAnnotations @@ -171,6 +172,38 @@ class PromoteAdminViewModelTest { assertTrue(viewModel.state.value.filteredMembers.isEmpty()) } + @Test + fun givenNoServerEligibleMembers_whenViewModelCreated_thenClientEligibleMembersAreShown() = runTest { + val clientEligibleUser = UserId("value0", "domain0") + val serverOnlyUser = UserId("value1", "domain1") + val (_, viewModel) = Arrangement() + .withEligibleMembers(listOf(member(0, "Alice", "alice"))) + .withConversationMembers(listOf(member(0, "Alice", "alice"), member(1, "Bob", "bob"))) + .arrange() + + advanceUntilIdle() + + val filteredMemberIds = viewModel.state.value.filteredMembers.map { it.userId } + assertEquals(listOf(clientEligibleUser), filteredMemberIds) + assertFalse(serverOnlyUser in filteredMemberIds) + } + + @Test + fun givenNavArgsWithEligibleMembers_whenViewModelCreated_thenServerMembersAreShownFromAllConversationMembers() = runTest { + val eligibleUser = UserId("value1", "domain1") + val (_, viewModel) = Arrangement() + .withNavEligibleMembers(listOf(eligibleUser)) + .withEligibleMembers(listOf(member(0, "Alice", "alice"))) + .withConversationMembers(listOf(member(0, "Alice", "alice"), member(1, "Bob", "bob"))) + .arrange() + + advanceUntilIdle() + + val filteredMembers = viewModel.state.value.filteredMembers + assertEquals(1, filteredMembers.size) + assertEquals(eligibleUser, filteredMembers.first().userId) + } + @Test fun givenUserSelectedAndPromoteSucceeds_whenPromoteAdminAndLeaveCalled_thenEmitSuccess() = runTest { val userId = UserId("user1", "wire.com") @@ -203,7 +236,7 @@ class PromoteAdminViewModelTest { fun givenUserSelectedAndLeaveFails_whenPromoteAdminAndLeaveCalled_thenEmitFailedToLeaveConversation() = runTest { val userId = UserId("user1", "wire.com") val (_, viewModel) = Arrangement() - .withPromoteAdminAndLeaveResult(PromoteAdminAndLeaveConversationUseCase.Result.FailedToLeaveConversation) + .withPromoteAdminAndLeaveResult(PromoteAdminAndLeaveConversationUseCase.Result.FailedToLeaveConversation()) .arrange() viewModel.onUserSelected(userId) @@ -213,6 +246,46 @@ class PromoteAdminViewModelTest { } } + @Test + fun givenUserSelectedAndLeaveReturnsServerEligibleMembers_whenPromoteAdminAndLeaveCalled_thenRefreshMembersAndEmitFailedToLeave() = + runTest { + val selectedUserId = UserId("value1", "domain1") + val returnedEligibleUserId = UserId("value2", "domain2") + val (_, viewModel) = Arrangement() + .withEligibleMembers(listOf(member(1, "Alice", "alice"))) + .withConversationMembers(listOf(member(1, "Alice", "alice"), member(2, "Bob", "bob"))) + .withPromoteAdminAndLeaveResult( + PromoteAdminAndLeaveConversationUseCase.Result.FailedToLeaveConversation(listOf(returnedEligibleUserId)) + ) + .arrange() + advanceUntilIdle() + + viewModel.onUserSelected(selectedUserId) + viewModel.actions.test { + viewModel.onPromoteAdminAndLeave() + assertEquals(PromoteAdminAction.FailedToLeaveConversation, awaitItem()) + advanceUntilIdle() + + assertNull(viewModel.state.value.selectedUserId) + assertEquals(listOf(returnedEligibleUserId), viewModel.state.value.filteredMembers.map { it.userId }) + } + } + + @Test + fun givenUserSelectedAndLeaveReturnsAdminlessWithoutMembers_whenPromoteAdminAndLeaveCalled_thenEmitFailedToLeaveConversation() = + runTest { + val userId = UserId("user1", "wire.com") + val (_, viewModel) = Arrangement() + .withPromoteAdminAndLeaveResult(PromoteAdminAndLeaveConversationUseCase.Result.FailedToLeaveConversation(emptyList())) + .arrange() + + viewModel.onUserSelected(userId) + viewModel.actions.test { + viewModel.onPromoteAdminAndLeave() + assertEquals(PromoteAdminAction.FailedToLeaveConversation, awaitItem()) + } + } + @Test fun givenNoUserSelected_whenPromoteAdminAndLeaveCalled_thenNoEventEmitted() = runTest { val (_, viewModel) = Arrangement().arrange() @@ -230,6 +303,9 @@ class PromoteAdminViewModelTest { @MockK lateinit var observeEligibleMembers: ObserveEligibleMembersForConversationAdminRoleUseCase + @MockK + lateinit var observeConversationMembers: ObserveConversationMembersUseCase + @MockK lateinit var promoteAdminAndLeave: PromoteAdminAndLeaveConversationUseCase @@ -238,6 +314,7 @@ class PromoteAdminViewModelTest { every { savedStateHandle.navArgs() } returns PromoteAdminNavArgs(ConversationId("conv1", "wire.com")) coEvery { observeEligibleMembers(any()) } returns flowOf(emptyList()) + coEvery { observeConversationMembers(any()) } returns flowOf(emptyList()) coEvery { promoteAdminAndLeave(any(), any()) } returns PromoteAdminAndLeaveConversationUseCase.Result.Success } @@ -245,12 +322,22 @@ class PromoteAdminViewModelTest { coEvery { observeEligibleMembers(any()) } returns flowOf(members) } + fun withConversationMembers(members: List) = apply { + coEvery { observeConversationMembers(any()) } returns flowOf(members) + } + + fun withNavEligibleMembers(eligibleMembers: List) = apply { + every { savedStateHandle.navArgs() } returns + PromoteAdminNavArgs(ConversationId("conv1", "wire.com"), eligibleMembers.toPromoteAdminEligibleMemberArgs()) + } + fun withPromoteAdminAndLeaveResult(result: PromoteAdminAndLeaveConversationUseCase.Result) = apply { coEvery { promoteAdminAndLeave(any(), any()) } returns result } fun arrange() = this to PromoteAdminViewModel( observeEligibleMembers = observeEligibleMembers, + observeConversationMembers = observeConversationMembers, promoteAdminAndLeave = promoteAdminAndLeave, dispatchers = TestDispatcherProvider(), savedStateHandle = savedStateHandle,