diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt index b82b5adc661..095daaacd2a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt @@ -83,6 +83,7 @@ class NewConversationViewModel @Inject constructor( ) private var pendingMLSGroupCreation: PendingMLSGroupCreation? = null + private var failedMLSGroupCreationId: ConversationId? = null var newGroupNameTextState: TextFieldState = TextFieldState() var newGroupState: GroupMetadataState by mutableStateOf(GroupMetadataState()) @@ -129,6 +130,7 @@ class NewConversationViewModel @Inject constructor( newGroupNameTextState.clearText() newGroupState = GroupMetadataState() pendingMLSGroupCreation = null + failedMLSGroupCreationId = null loadDefaultProtocol() observeAllowanceOfAppsUsageInitialState() createGroupState = CreateGroupState.Default @@ -205,12 +207,32 @@ class NewConversationViewModel @Inject constructor( createGroupState = CreateGroupState.Default } + fun discardGroupCreation() { + val conversationId = failedMLSGroupCreationId + if (conversationId == null) { + createGroupState = CreateGroupState.Discarded + return + } + + createGroupState = CreateGroupState.Discarding + viewModelScope.launch { + if (createRegularGroup.discardPendingMLSGroupCreation(conversationId)) { + failedMLSGroupCreationId = null + pendingMLSGroupCreation = null + createGroupState = CreateGroupState.Discarded + } else { + createGroupState = CreateGroupState.Error.Unknown + } + } + } + fun retryPendingMLSGroupCreation() { val pendingCreation = pendingMLSGroupCreation ?: return createGroupState = CreateGroupState.Error.PendingMLSCreation(isRetrying = true) viewModelScope.launch { when (val result = createRegularGroup.retryPendingMLSGroupCreation(pendingCreation.conversationId)) { is ConversationCreationResult.Success, + is ConversationCreationResult.BackendConflictFailure, is ConversationCreationResult.PendingMLSGroupCreation -> handleNewGroupCreationResult(result, pendingCreation.attempt) @@ -376,6 +398,7 @@ class NewConversationViewModel @Inject constructor( return when (result) { is ConversationCreationResult.Success -> { pendingMLSGroupCreation = null + failedMLSGroupCreationId = null newGroupState = newGroupState.copy(isLoading = false) createGroupState = CreateGroupState.Created(result.conversation.id) } @@ -410,6 +433,8 @@ class NewConversationViewModel @Inject constructor( } is ConversationCreationResult.BackendConflictFailure -> { + pendingMLSGroupCreation = null + failedMLSGroupCreationId = result.conversationId groupOptionsState = groupOptionsState.copy(isLoading = false) newGroupState = newGroupState.copy(isLoading = false) createGroupState = CreateGroupState.Error.ConflictedBackends(result.domains) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupState.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupState.kt index 0b777d5203b..e816108de92 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupState.kt @@ -22,6 +22,8 @@ import com.wire.kalium.logic.data.id.ConversationId sealed interface CreateGroupState { data object Default : CreateGroupState + data object Discarding : CreateGroupState + data object Discarded : CreateGroupState sealed interface Error : CreateGroupState { data object Unknown : Error diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupOptions/GroupOptionsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupOptions/GroupOptionsScreen.kt index 2f5ca8c4c8b..caa39e18046 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupOptions/GroupOptionsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupOptions/GroupOptionsScreen.kt @@ -89,8 +89,10 @@ internal fun GroupOptionRouteScreen( onNavigateBack: () -> Unit, ) { LaunchedEffect(newConversationViewModel.createGroupState) { - (newConversationViewModel.createGroupState as? CreateGroupState.Created)?.let { - onConversationCreated(it.conversationId) + when (val state = newConversationViewModel.createGroupState) { + is CreateGroupState.Created -> onConversationCreated(state.conversationId) + CreateGroupState.Discarded -> onDiscard() + else -> Unit } } @@ -118,8 +120,7 @@ internal fun GroupOptionRouteScreen( onEditParticipants() }, onDiscardGroupCreationClick = { - newConversationViewModel.onCreateGroupErrorDismiss() - onDiscard() + newConversationViewModel.discardGroupCreation() }, onRetryPendingCreation = newConversationViewModel::retryPendingMLSGroupCreation, onErrorDismissed = newConversationViewModel::onCreateGroupErrorDismiss, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupname/NewGroupNameScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupname/NewGroupNameScreen.kt index c73850ad647..c539ff4201f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupname/NewGroupNameScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupname/NewGroupNameScreen.kt @@ -47,8 +47,10 @@ internal fun NewGroupNameRouteScreen( newConversationViewModel.observeGroupNameChanges() } LaunchedEffect(newConversationViewModel.createGroupState) { - (newConversationViewModel.createGroupState as? CreateGroupState.Created)?.let { - onConversationCreated(it.conversationId) + when (val state = newConversationViewModel.createGroupState) { + is CreateGroupState.Created -> onConversationCreated(state.conversationId) + CreateGroupState.Discarded -> onDiscard() + else -> Unit } } GroupNameScreen( @@ -70,16 +72,14 @@ internal fun NewGroupNameRouteScreen( onDismiss = newConversationViewModel::onCreateGroupErrorDismiss, onRetryPendingCreation = newConversationViewModel::retryPendingMLSGroupCreation, onPendingCreationAcknowledged = { - newConversationViewModel.onCreateGroupErrorDismiss() - onDiscard() + newConversationViewModel.discardGroupCreation() }, onEditParticipantsList = { newConversationViewModel.onCreateGroupErrorDismiss() onEditParticipants() }, onCancel = { - newConversationViewModel.onCreateGroupErrorDismiss() - onDiscard() + newConversationViewModel.discardGroupCreation() }, ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt index 6d5c38ee8d0..ac903ec2abc 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt @@ -60,6 +60,7 @@ internal class NewConversationViewModelArrangement { // Default empty values coEvery { isMLSEnabledUseCase() } returns true coEvery { createRegularGroup(any(), any(), any()) } returns ConversationCreationResult.Success(CONVERSATION) + coEvery { createRegularGroup.discardPendingMLSGroupCreation(any()) } returns true coEvery { observeChannelsCreationPermissionUseCase() } returns flowOf(ChannelCreationPermission.Forbidden) coEvery { getDefaultProtocol() } returns SupportedProtocol.PROTEUS coEvery { isWireCellsEnabled() } returns false @@ -189,6 +190,16 @@ internal class NewConversationViewModelArrangement { ) } + fun withBackendConflictOnCreatingGroup(domains: List) = apply { + coEvery { createRegularGroup(any(), any(), any()) } returns + ConversationCreationResult.BackendConflictFailure(domains) + } + + fun withBackendConflictCleanupFallbackOnCreatingGroup(domains: List) = apply { + coEvery { createRegularGroup(any(), any(), any()) } returns + ConversationCreationResult.BackendConflictFailure(domains, CONVERSATION_ID) + } + fun withPendingMLSGroupCreation() = apply { coEvery { createRegularGroup(any(), any(), any()) } returns ConversationCreationResult.PendingMLSGroupCreation( CONVERSATION_ID, @@ -203,6 +214,11 @@ internal class NewConversationViewModelArrangement { ConversationCreationResult.UnknownFailure(CoreFailure.Unknown(UnsupportedOperationException("retry failed"))) } + fun withPendingMLSGroupCreationRetryBackendConflict(domains: List) = apply { + coEvery { createRegularGroup.retryPendingMLSGroupCreation(CONVERSATION_ID) } returns + ConversationCreationResult.BackendConflictFailure(domains) + } + fun withConflictingBackendsFailure() = apply { createGroupState = CreateGroupState.Error.ConflictedBackends(listOf("bella.wire.link", "foma.wire.link")) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt index 64e9941a4fe..edeb12cce32 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt @@ -88,6 +88,49 @@ class NewConversationViewModelTest { viewModel.createGroupState shouldBeEqualTo CreateGroupState.Error.Unknown } + @Test + fun `given backend conflict, when creating group, then should show conflicting backends error`() = runTest { + val domains = listOf("backend-a.example", "backend-b.example") + val (arrangement, viewModel) = NewConversationViewModelArrangement() + .withGetSelfUser(isTeamMember = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .withBackendConflictOnCreatingGroup(domains) + .arrange() + + viewModel.createGroup() + advanceUntilIdle() + + viewModel.createGroupState shouldBeEqualTo CreateGroupState.Error.ConflictedBackends(domains) + + viewModel.discardGroupCreation() + advanceUntilIdle() + + viewModel.createGroupState shouldBeEqualTo CreateGroupState.Discarded + coVerify(exactly = 0) { + arrangement.createRegularGroup.discardPendingMLSGroupCreation(NewConversationViewModelArrangement.CONVERSATION_ID) + } + } + + @Test + fun `given backend conflict cleanup fallback, when discarding, then should retry cleanup`() = runTest { + val domains = listOf("backend-a.example", "backend-b.example") + val (arrangement, viewModel) = NewConversationViewModelArrangement() + .withGetSelfUser(isTeamMember = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .withBackendConflictCleanupFallbackOnCreatingGroup(domains) + .arrange() + + viewModel.createGroup() + advanceUntilIdle() + viewModel.discardGroupCreation() + advanceUntilIdle() + + viewModel.createGroupState shouldBeEqualTo CreateGroupState.Discarded + coVerify(exactly = 1) { + arrangement.createRegularGroup.discardPendingMLSGroupCreation(NewConversationViewModelArrangement.CONVERSATION_ID) + } + } + @Test fun `given MLS establish fails after creation, when retrying, then existing conversation is reused`() = runTest { val (arrangement, viewModel) = NewConversationViewModelArrangement() @@ -130,6 +173,30 @@ class NewConversationViewModelTest { viewModel.createGroupState shouldBeEqualTo CreateGroupState.Error.PendingMLSCreation() } + @Test + fun `given pending MLS creation, when retry finds backend conflict, then should show conflicting backends error`() = runTest { + val domains = listOf("backend-a.example", "backend-b.example") + val (arrangement, viewModel) = NewConversationViewModelArrangement() + .withGetSelfUser(isTeamMember = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .withPendingMLSGroupCreation() + .withPendingMLSGroupCreationRetryBackendConflict(domains) + .arrange() + advanceUntilIdle() + + viewModel.createGroup() + advanceUntilIdle() + viewModel.retryPendingMLSGroupCreation() + advanceUntilIdle() + + viewModel.createGroupState shouldBeEqualTo CreateGroupState.Error.ConflictedBackends(domains) + viewModel.retryPendingMLSGroupCreation() + advanceUntilIdle() + coVerify(exactly = 1) { + arrangement.createRegularGroup.retryPendingMLSGroupCreation(NewConversationViewModelArrangement.CONVERSATION_ID) + } + } + @Test fun `given no failure, when creating group, then options state should have no error`() = runTest { val (_, viewModel) = NewConversationViewModelArrangement() diff --git a/kalium b/kalium index ddcdaba4c10..b3e819df05b 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ddcdaba4c1034080f65eef9456cee1a23e512dac +Subproject commit b3e819df05b39ebc1633aae08317f60bd59b2e4c