diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt index cabcd7ace28..efafac1a055 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt @@ -34,6 +34,7 @@ import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.ui.UIText import com.wire.android.util.ui.toUIText import com.wire.kalium.common.error.StorageFailure +import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper @@ -41,6 +42,8 @@ import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.createconversation.ConversationCreationResult +import com.wire.kalium.logic.feature.conversation.createconversation.CreateRegularGroupUseCase import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory @@ -52,6 +55,7 @@ class ConversationInfoViewModel @AssistedInject constructor( private val qualifiedIdMapper: QualifiedIdMapper, @Assisted val savedStateHandle: SavedStateHandle, private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val createRegularGroup: CreateRegularGroupUseCase, private val fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase, private val isWireCellFeatureEnabled: IsWireCellsEnabledUseCase, @CurrentAccount private val selfUserId: UserId, @@ -67,6 +71,8 @@ class ConversationInfoViewModel @AssistedInject constructor( var conversationInfoViewState by mutableStateOf(ConversationInfoViewState(conversationId)) + private var pendingMLSCreationRetryStarted = false + init { fetchMLSVerificationStatus() } @@ -107,6 +113,8 @@ class ConversationInfoViewModel @AssistedInject constructor( } private suspend fun handleConversationDetails(conversationDetails: ConversationDetails) { + retryPendingMLSCreationIfNeeded(conversationDetails) + val (isConversationUnavailable, _) = when (conversationDetails) { is ConversationDetails.OneOne -> conversationDetails.otherUser @@ -133,6 +141,23 @@ class ConversationInfoViewModel @AssistedInject constructor( ) } + private fun retryPendingMLSCreationIfNeeded(conversationDetails: ConversationDetails) { + val protocol = conversationDetails.conversation.protocol as? Conversation.ProtocolInfo.MLSCapable + if (protocol?.groupState != Conversation.ProtocolInfo.MLSCapable.GroupState.PENDING_CREATION || + pendingMLSCreationRetryStarted + ) { + return + } + + pendingMLSCreationRetryStarted = true + viewModelScope.launch { + when (val result = createRegularGroup.retryPendingMLSGroupCreation(conversationDetails.conversation.id)) { + is ConversationCreationResult.Success -> Unit + else -> appLogger.w("Failed to establish pending MLS conversation after opening it: $result") + } + } + } + private fun getAccentId(conversationDetails: ConversationDetails): Int { return if (conversationDetails is ConversationDetails.OneOne) { conversationDetails.otherUser.accentId 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 6926d43bdc2..b82b5adc661 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 @@ -40,6 +40,7 @@ import com.wire.android.model.Contact import com.wire.android.util.AppsUtil import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.CreateConversationParam +import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.isExternal import com.wire.kalium.logic.feature.channels.ChannelCreationPermission @@ -69,6 +70,20 @@ class NewConversationViewModel @Inject constructor( private val observeIsAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase ) : ViewModel() { + private data class GroupCreationAttempt( + val name: String, + val userIdList: List, + val options: CreateConversationParam, + val isChannel: Boolean, + ) + + private data class PendingMLSGroupCreation( + val conversationId: ConversationId, + val attempt: GroupCreationAttempt, + ) + + private var pendingMLSGroupCreation: PendingMLSGroupCreation? = null + var newGroupNameTextState: TextFieldState = TextFieldState() var newGroupState: GroupMetadataState by mutableStateOf(GroupMetadataState()) @@ -113,6 +128,7 @@ class NewConversationViewModel @Inject constructor( fun resetState() { newGroupNameTextState.clearText() newGroupState = GroupMetadataState() + pendingMLSGroupCreation = null loadDefaultProtocol() observeAllowanceOfAppsUsageInitialState() createGroupState = CreateGroupState.Default @@ -189,6 +205,25 @@ class NewConversationViewModel @Inject constructor( createGroupState = CreateGroupState.Default } + 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.PendingMLSGroupCreation -> + handleNewGroupCreationResult(result, pendingCreation.attempt) + + else -> { + appLogger.w("Failed to retry pending MLS conversation creation: $result") + groupOptionsState = groupOptionsState.copy(isLoading = false) + newGroupState = newGroupState.copy(isLoading = false) + createGroupState = CreateGroupState.Error.PendingMLSCreation() + } + } + } + } + fun onAllowGuestStatusChanged(status: Boolean) { groupOptionsState = groupOptionsState.copy(isAllowGuestEnabled = status) } @@ -258,7 +293,7 @@ class NewConversationViewModel @Inject constructor( fun createChannel() { viewModelScope.launch { groupOptionsState = groupOptionsState.copy(isLoading = true) - val result = createChannel( + val attempt = GroupCreationAttempt( name = newGroupNameTextState.text.toString().trim(), userIdList = newGroupState.selectedUsers.map { UserId(it.id, it.domain) }, options = CreateConversationParam().copy( @@ -273,16 +308,18 @@ class NewConversationViewModel @Inject constructor( channelAddPermission = newGroupState.channelAddPermissionType.toDomainEnum(), wireCellEnabled = groupOptionsState.isWireCellsEnabled ?: false, // TODO: include channel history type - ) + ), + isChannel = true, ) - handleNewGroupCreationResult(result) + val result = createOrRetryGroup(attempt) + handleNewGroupCreationResult(result, attempt) } } private fun createGroupForPersonalAccounts() { viewModelScope.launch { newGroupState = newGroupState.copy(isLoading = true) - val result = createRegularGroup( + val attempt = GroupCreationAttempt( name = newGroupNameTextState.text.toString().trim(), userIdList = newGroupState.selectedUsers.map { UserId(it.id, it.domain) }, options = CreateConversationParam().copy( @@ -290,9 +327,11 @@ class NewConversationViewModel @Inject constructor( accessRole = Conversation.defaultGroupAccessRoles, access = Conversation.defaultGroupAccess, wireCellEnabled = groupOptionsState.isWireCellsEnabled ?: false, - ) + ), + isChannel = false, ) - handleNewGroupCreationResult(result) + val result = createOrRetryGroup(attempt) + handleNewGroupCreationResult(result, attempt) } } @@ -300,7 +339,7 @@ class NewConversationViewModel @Inject constructor( if (shouldCheckGuests && checkIfGuestAdded()) return viewModelScope.launch { groupOptionsState = groupOptionsState.copy(isLoading = true) - val result = createRegularGroup( + val attempt = GroupCreationAttempt( name = newGroupNameTextState.text.toString().trim(), // TODO: change the id in Contact to UserId instead of String userIdList = newGroupState.selectedUsers.map { UserId(it.id, it.domain) }, @@ -314,19 +353,41 @@ class NewConversationViewModel @Inject constructor( nonTeamMembersAllowed = groupOptionsState.isAllowGuestEnabled ), access = Conversation.accessFor(groupOptionsState.isAllowGuestEnabled), - ) + ), + isChannel = false, ) - handleNewGroupCreationResult(result) + val result = createOrRetryGroup(attempt) + handleNewGroupCreationResult(result, attempt) } } - private fun handleNewGroupCreationResult(result: ConversationCreationResult) { + private suspend fun createOrRetryGroup(attempt: GroupCreationAttempt): ConversationCreationResult { + val pendingCreation = pendingMLSGroupCreation + return if (pendingCreation?.attempt == attempt) { + createRegularGroup.retryPendingMLSGroupCreation(pendingCreation.conversationId) + } else if (attempt.isChannel) { + createChannel(attempt.name, attempt.userIdList, attempt.options) + } else { + createRegularGroup(attempt.name, attempt.userIdList, attempt.options) + } + } + + private fun handleNewGroupCreationResult(result: ConversationCreationResult, attempt: GroupCreationAttempt) { return when (result) { is ConversationCreationResult.Success -> { + pendingMLSGroupCreation = null newGroupState = newGroupState.copy(isLoading = false) createGroupState = CreateGroupState.Created(result.conversation.id) } + is ConversationCreationResult.PendingMLSGroupCreation -> { + pendingMLSGroupCreation = PendingMLSGroupCreation(result.conversationId, attempt) + appLogger.w("MLS conversation was created but still needs to be established: ${result.cause}") + groupOptionsState = groupOptionsState.copy(isLoading = false) + newGroupState = newGroupState.copy(isLoading = false) + createGroupState = CreateGroupState.Error.PendingMLSCreation() + } + ConversationCreationResult.Forbidden -> { appLogger.d("Can't create conversation due to Insufficient permissions") groupOptionsState = groupOptionsState.copy(isLoading = false) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupErrorDialog.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupErrorDialog.kt index 6ec64fb78c3..773359676fe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupErrorDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupErrorDialog.kt @@ -25,8 +25,10 @@ import com.wire.android.ui.common.DialogTextSuffixLink import com.wire.android.ui.common.WireDialog import com.wire.android.ui.common.WireDialogButtonProperties import com.wire.android.ui.common.WireDialogButtonType +import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.typography +import com.wire.android.ui.common.wireDialogPropertiesBuilder import com.wire.android.ui.theme.WireTheme import com.wire.android.util.DialogErrorStrings import com.wire.android.util.SupportPage @@ -38,9 +40,20 @@ import com.wire.android.util.ui.stringWithStyledArgs fun CreateGroupErrorDialog( error: CreateGroupState.Error, onDismiss: () -> Unit, + onRetryPendingCreation: () -> Unit, + onPendingCreationAcknowledged: () -> Unit, onEditParticipantsList: () -> Unit, onCancel: () -> Unit ) { + if (error is CreateGroupState.Error.PendingMLSCreation) { + PendingMLSGroupCreationDialog( + isRetrying = error.isRetrying, + onRetry = onRetryPendingCreation, + onAcknowledged = onPendingCreationAcknowledged, + ) + return + } + val (dialogStrings, dialogSuffixLink) = when (error) { is CreateGroupState.Error.LackingConnection -> DialogErrorStrings( title = stringResource(R.string.error_no_network_title), @@ -52,6 +65,8 @@ fun CreateGroupErrorDialog( message = stringResource(R.string.error_unknown_message), ) to null + is CreateGroupState.Error.PendingMLSCreation -> error("Pending MLS creation is handled separately") + is CreateGroupState.Error.ConflictedBackends -> DialogErrorStrings( title = stringResource(id = R.string.conversation_can_not_be_created_title), annotatedMessage = LocalContext.current.resources.stringWithStyledArgs( @@ -102,11 +117,43 @@ fun CreateGroupErrorDialog( ) } +@Composable +private fun PendingMLSGroupCreationDialog( + isRetrying: Boolean, + onRetry: () -> Unit, + onAcknowledged: () -> Unit, +) { + val buttonState = if (isRetrying) WireButtonState.Disabled else WireButtonState.Default + WireDialog( + title = stringResource(R.string.conversation_creation_pending_title), + text = stringResource(R.string.conversation_creation_pending_message), + onDismiss = onAcknowledged, + buttonsHorizontalAlignment = false, + optionButton1Properties = WireDialogButtonProperties( + onClick = onRetry, + text = stringResource(R.string.label_retry), + type = WireDialogButtonType.Primary, + state = buttonState, + loading = isRetrying, + ), + optionButton2Properties = WireDialogButtonProperties( + onClick = onAcknowledged, + text = stringResource(R.string.label_ok), + type = WireDialogButtonType.Secondary, + state = buttonState, + ), + properties = wireDialogPropertiesBuilder( + dismissOnBackPress = !isRetrying, + dismissOnClickOutside = !isRetrying, + ), + ) +} + @PreviewMultipleThemes @Composable private fun PreviewCreateGroupErrorDialogLackingConnection() { WireTheme { - CreateGroupErrorDialog(CreateGroupState.Error.LackingConnection, {}, {}, {}) + CreateGroupErrorDialog(CreateGroupState.Error.LackingConnection, {}, {}, {}, {}, {}) } } @@ -114,7 +161,7 @@ private fun PreviewCreateGroupErrorDialogLackingConnection() { @Composable private fun PreviewCreateGroupErrorDialogUnknown() { WireTheme { - CreateGroupErrorDialog(CreateGroupState.Error.Unknown, {}, {}, {}) + CreateGroupErrorDialog(CreateGroupState.Error.Unknown, {}, {}, {}, {}, {}) } } @@ -122,6 +169,14 @@ private fun PreviewCreateGroupErrorDialogUnknown() { @Composable private fun PreviewCreateGroupErrorDialogConflictedBackends() { WireTheme { - CreateGroupErrorDialog(CreateGroupState.Error.ConflictedBackends(listOf("some.com", "other.com")), {}, {}, {}) + CreateGroupErrorDialog(CreateGroupState.Error.ConflictedBackends(listOf("some.com", "other.com")), {}, {}, {}, {}, {}) + } +} + +@PreviewMultipleThemes +@Composable +private fun PreviewCreateGroupErrorDialogPendingMLS() { + WireTheme { + CreateGroupErrorDialog(CreateGroupState.Error.PendingMLSCreation(), {}, {}, {}, {}, {}) } } 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 a3b557c963a..0b777d5203b 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 @@ -27,6 +27,7 @@ sealed interface CreateGroupState { data object Unknown : Error data object Forbidden : Error data object LackingConnection : Error + data class PendingMLSCreation(val isRetrying: Boolean = false) : Error data class ConflictedBackends(val domains: List) : Error val isConflictedBackends get() = this is ConflictedBackends 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 b665fad48cb..0ff0c94f6a6 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 @@ -127,6 +127,7 @@ fun GroupOptionScreen( newConversationViewModel.onCreateGroupErrorDismiss() navigator.navigate(NavigationCommand(HomeScreenDestination, BackStackMode.CLEAR_WHOLE)) }, + onRetryPendingCreation = newConversationViewModel::retryPendingMLSGroupCreation, onErrorDismissed = newConversationViewModel::onCreateGroupErrorDismiss, onEnableWireCellChanged = newConversationViewModel::onEnableWireCellChanged ) @@ -150,6 +151,7 @@ private fun GroupOptionScreenContent( onErrorDismissed: () -> Unit, onEditParticipantsClick: () -> Unit, onDiscardGroupCreationClick: () -> Unit, + onRetryPendingCreation: () -> Unit, onBackPressed: () -> Unit, channelsHistoryOptionsEnabled: Boolean = BuildConfig.CHANNELS_HISTORY_OPTIONS_ENABLED, mlsReadReceiptsEnabled: Boolean = BuildConfig.MLS_READ_RECEIPTS_ENABLED, @@ -193,7 +195,14 @@ private fun GroupOptionScreenContent( } (createGroupState as? CreateGroupState.Error)?.let { - CreateGroupErrorDialog(it, onErrorDismissed, onEditParticipantsClick, onDiscardGroupCreationClick) + CreateGroupErrorDialog( + error = it, + onDismiss = onErrorDismissed, + onRetryPendingCreation = onRetryPendingCreation, + onPendingCreationAcknowledged = onDiscardGroupCreationClick, + onEditParticipantsList = onEditParticipantsClick, + onCancel = onDiscardGroupCreationClick, + ) } if (showAllowGuestsDialog) { AllowGuestsDialog(onAllowGuestsDialogDismissed, onNotAllowGuestsClicked, onAllowGuestsClicked) @@ -465,6 +474,7 @@ private fun PreviewGroupOptionScreen( onErrorDismissed = {}, onEditParticipantsClick = {}, onDiscardGroupCreationClick = {}, + onRetryPendingCreation = {}, onBackPressed = {}, channelsHistoryOptionsEnabled = channelsHistoryOptionsEnabled, mlsReadReceiptsEnabled = mlsReadReceiptsEnabled, 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 b3f947a2bfe..1d8bc1aac4d 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 @@ -73,6 +73,11 @@ fun NewGroupNameScreen( CreateGroupErrorDialog( error = it, onDismiss = newConversationViewModel::onCreateGroupErrorDismiss, + onRetryPendingCreation = newConversationViewModel::retryPendingMLSGroupCreation, + onPendingCreationAcknowledged = { + newConversationViewModel.onCreateGroupErrorDismiss() + navigator.navigate(NavigationCommand(HomeScreenDestination, BackStackMode.CLEAR_WHOLE)) + }, onEditParticipantsList = { newConversationViewModel.onCreateGroupErrorDismiss() navigator.navigate(NavigationCommand(NewGroupConversationSearchPeopleScreenDestination, BackStackMode.UPDATE_EXISTED)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 079a4fe6073..099d37cf387 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,6 +87,8 @@ Messaging and Retry + Conversation isn’t ready yet + We couldn’t finish setting up the conversation. You can try again now, or we’ll keep trying in the background. *Draft* Show password diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt index 421a0ff0b0d..18b54b3e0e6 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt @@ -31,6 +31,8 @@ import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.createconversation.ConversationCreationResult +import com.wire.kalium.logic.feature.conversation.createconversation.CreateRegularGroupUseCase import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -56,6 +58,9 @@ class ConversationInfoViewModelArrangement { @MockK lateinit var observeConversationDetails: ObserveConversationDetailsUseCase + @MockK + lateinit var createRegularGroup: CreateRegularGroupUseCase + @MockK lateinit var fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase @@ -67,6 +72,7 @@ class ConversationInfoViewModelArrangement { qualifiedIdMapper = qualifiedIdMapper, savedStateHandle = savedStateHandle, observeConversationDetails = observeConversationDetails, + createRegularGroup = createRegularGroup, fetchConversationMLSVerificationStatus = fetchConversationMLSVerificationStatus, selfUserId = TestUser.SELF_USER_ID, isWireCellFeatureEnabled = isCellsEnabled, @@ -86,6 +92,7 @@ class ConversationInfoViewModelArrangement { } coEvery { fetchConversationMLSVerificationStatus.invoke(any()) } returns Unit coEvery { isCellsEnabled() } returns false + coEvery { createRegularGroup.retryPendingMLSGroupCreation(any()) } returns ConversationCreationResult.SyncFailure } suspend fun withConversationDetailUpdate(conversationDetails: ConversationDetails) = apply { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt index 9e3686ea548..a0eb77b6fb0 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt @@ -20,13 +20,16 @@ package com.wire.android.ui.home.conversations.info import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension +import com.wire.android.framework.TestConversation import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.composer.mockConversationDetailsGroup import com.wire.android.ui.home.conversations.composer.withMockConversationDetailsOneOnOne import com.wire.android.util.EMPTY import com.wire.android.util.ui.UIText import com.wire.kalium.common.error.StorageFailure +import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import io.mockk.coVerify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceUntilIdle @@ -40,6 +43,42 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(NavigationTestExtension::class) class ConversationInfoViewModelTest { + @Test + fun `given pending MLS creation, when opening conversation, then retry creation once`() = runTest { + val pendingProtocol = TestConversation.MLS_PROTOCOL_INFO.copy( + groupState = Conversation.ProtocolInfo.MLSCapable.GroupState.PENDING_CREATION + ) + val pendingConversationDetails = mockConversationDetailsGroup("Pending conversation").let { details -> + details.copy(conversation = details.conversation.copy(protocol = pendingProtocol)) + } + val (arrangement, viewModel) = ConversationInfoViewModelArrangement().arrange() + + launch { viewModel.observeConversationDetails() }.run { + arrangement.withConversationDetailUpdate(pendingConversationDetails) + arrangement.withConversationDetailUpdate(pendingConversationDetails) + advanceUntilIdle() + + coVerify(exactly = 1) { + arrangement.createRegularGroup.retryPendingMLSGroupCreation(pendingConversationDetails.conversation.id) + } + cancel() + } + } + + @Test + fun `given established conversation, when opening conversation, then do not retry creation`() = runTest { + val conversationDetails = mockConversationDetailsGroup("Established conversation") + val (arrangement, viewModel) = ConversationInfoViewModelArrangement().arrange() + + launch { viewModel.observeConversationDetails() }.run { + arrangement.withConversationDetailUpdate(conversationDetails) + advanceUntilIdle() + + coVerify(exactly = 0) { arrangement.createRegularGroup.retryPendingMLSGroupCreation(any()) } + cancel() + } + } + @Test fun `given a self mentioned user, when getting user data, then return valid result`() = runTest { // Given 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 6b790933643..6d5c38ee8d0 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 @@ -189,6 +189,20 @@ internal class NewConversationViewModelArrangement { ) } + fun withPendingMLSGroupCreation() = apply { + coEvery { createRegularGroup(any(), any(), any()) } returns ConversationCreationResult.PendingMLSGroupCreation( + CONVERSATION_ID, + CoreFailure.Unknown(UnsupportedOperationException("establish failed")) + ) + coEvery { createRegularGroup.retryPendingMLSGroupCreation(CONVERSATION_ID) } returns + ConversationCreationResult.Success(CONVERSATION) + } + + fun withPendingMLSGroupCreationRetryFailure() = apply { + coEvery { createRegularGroup.retryPendingMLSGroupCreation(CONVERSATION_ID) } returns + ConversationCreationResult.UnknownFailure(CoreFailure.Unknown(UnsupportedOperationException("retry failed"))) + } + 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 4593f960fbd..64e9941a4fe 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,48 @@ class NewConversationViewModelTest { viewModel.createGroupState shouldBeEqualTo CreateGroupState.Error.Unknown } + @Test + fun `given MLS establish fails after creation, when retrying, then existing conversation is reused`() = runTest { + val (arrangement, viewModel) = NewConversationViewModelArrangement() + .withGetSelfUser(isTeamMember = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .withPendingMLSGroupCreation() + .arrange() + advanceUntilIdle() + + viewModel.createGroup() + advanceUntilIdle() + + viewModel.createGroupState shouldBeEqualTo CreateGroupState.Error.PendingMLSCreation() + + viewModel.retryPendingMLSGroupCreation() + advanceUntilIdle() + + viewModel.createGroupState shouldBeEqualTo CreateGroupState.Created(CONVERSATION.id) + coVerify(exactly = 1) { arrangement.createRegularGroup(any(), any(), any()) } + coVerify(exactly = 1) { + arrangement.createRegularGroup.retryPendingMLSGroupCreation(NewConversationViewModelArrangement.CONVERSATION_ID) + } + } + + @Test + fun `given pending MLS creation, when retry fails, then pending creation dialog remains available`() = runTest { + val (_, viewModel) = NewConversationViewModelArrangement() + .withGetSelfUser(isTeamMember = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .withPendingMLSGroupCreation() + .withPendingMLSGroupCreationRetryFailure() + .arrange() + advanceUntilIdle() + + viewModel.createGroup() + advanceUntilIdle() + viewModel.retryPendingMLSGroupCreation() + advanceUntilIdle() + + viewModel.createGroupState shouldBeEqualTo CreateGroupState.Error.PendingMLSCreation() + } + @Test fun `given no failure, when creating group, then options state should have no error`() = runTest { val (_, viewModel) = NewConversationViewModelArrangement() diff --git a/app/stability/app-devDebug.stability b/app/stability/app-devDebug.stability index 71630db5281..7ef7ffa32fa 100644 --- a/app/stability/app-devDebug.stability +++ b/app/stability/app-devDebug.stability @@ -3948,6 +3948,13 @@ private fun com.wire.android.ui.debug.securityproviders.NetworkSection(network: params: - network: RUNTIME (requires runtime check) +@Composable +private fun com.wire.android.ui.debug.securityproviders.SQLSecuritySection(security: com.wire.android.ui.debug.securityproviders.DatabaseSecurityInfo): kotlin.Unit + skippable: true + restartable: true + params: + - security: STABLE (class with no mutable properties) + @Composable public fun com.wire.android.ui.debug.securityproviders.SecurityProvidersScreen(navigator: com.wire.android.navigation.Navigator, modifier: androidx.compose.ui.Modifier, viewModel: com.wire.android.ui.debug.securityproviders.SecurityProvidersViewModel): kotlin.Unit skippable: false @@ -9143,12 +9150,14 @@ public fun com.wire.android.ui.home.newconversation.common.ContinueButton(onCont - elevation: STABLE (marked @Stable or @Immutable) @Composable -public fun com.wire.android.ui.home.newconversation.common.CreateGroupErrorDialog(error: com.wire.android.ui.home.newconversation.common.CreateGroupState.Error, onDismiss: kotlin.Function0, onEditParticipantsList: kotlin.Function0, onCancel: kotlin.Function0): kotlin.Unit +public fun com.wire.android.ui.home.newconversation.common.CreateGroupErrorDialog(error: com.wire.android.ui.home.newconversation.common.CreateGroupState.Error, onDismiss: kotlin.Function0, onRetryPendingCreation: kotlin.Function0, onPendingCreationAcknowledged: kotlin.Function0, onEditParticipantsList: kotlin.Function0, onCancel: kotlin.Function0): kotlin.Unit skippable: true restartable: true params: - error: STABLE (class with no mutable properties) - onDismiss: STABLE (function type) + - onRetryPendingCreation: STABLE (function type) + - onPendingCreationAcknowledged: STABLE (function type) - onEditParticipantsList: STABLE (function type) - onCancel: STABLE (function type) @@ -9165,6 +9174,15 @@ public fun com.wire.android.ui.home.newconversation.common.CreateRegularGroupOrC - firstVisibleButtonModifier: STABLE (marked @Stable or @Immutable) - elevation: STABLE (marked @Stable or @Immutable) +@Composable +private fun com.wire.android.ui.home.newconversation.common.PendingMLSGroupCreationDialog(isRetrying: kotlin.Boolean, onRetry: kotlin.Function0, onAcknowledged: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - isRetrying: STABLE (primitive type) + - onRetry: STABLE (function type) + - onAcknowledged: STABLE (function type) + @Composable public fun com.wire.android.ui.home.newconversation.common.SelfDeletionTimerIcon(selfDeletionTimer: com.wire.kalium.logic.data.message.SelfDeletionTimer, isDisabled: kotlin.Boolean, modifier: androidx.compose.ui.Modifier, onSelfDeletionTimerClicked: kotlin.Function0): kotlin.Unit skippable: false @@ -9243,7 +9261,7 @@ public fun com.wire.android.ui.home.newconversation.groupOptions.GroupOptionScre - newConversationViewModel: UNSTABLE (has mutable properties or unstable members) @Composable -private fun com.wire.android.ui.home.newconversation.groupOptions.GroupOptionScreenContent(groupOptionState: com.wire.android.ui.home.newconversation.groupOptions.GroupOptionState, createGroupState: com.wire.android.ui.home.newconversation.common.CreateGroupState, groupMetadataState: com.wire.android.ui.common.groupname.GroupMetadataState, onAccessClicked: kotlin.Function0, onHistoryClicked: kotlin.Function0, onAllowGuestChanged: kotlin.Function1, onAllowServicesChanged: kotlin.Function1, onReadReceiptChanged: kotlin.Function1, onEnableWireCellChanged: kotlin.Function1, onContinuePressed: kotlin.Function0, onAllowGuestsDialogDismissed: kotlin.Function0, onNotAllowGuestsClicked: kotlin.Function0, onAllowGuestsClicked: kotlin.Function0, onErrorDismissed: kotlin.Function0, onEditParticipantsClick: kotlin.Function0, onDiscardGroupCreationClick: kotlin.Function0, onBackPressed: kotlin.Function0, channelsHistoryOptionsEnabled: kotlin.Boolean, mlsReadReceiptsEnabled: kotlin.Boolean): kotlin.Unit +private fun com.wire.android.ui.home.newconversation.groupOptions.GroupOptionScreenContent(groupOptionState: com.wire.android.ui.home.newconversation.groupOptions.GroupOptionState, createGroupState: com.wire.android.ui.home.newconversation.common.CreateGroupState, groupMetadataState: com.wire.android.ui.common.groupname.GroupMetadataState, onAccessClicked: kotlin.Function0, onHistoryClicked: kotlin.Function0, onAllowGuestChanged: kotlin.Function1, onAllowServicesChanged: kotlin.Function1, onReadReceiptChanged: kotlin.Function1, onEnableWireCellChanged: kotlin.Function1, onContinuePressed: kotlin.Function0, onAllowGuestsDialogDismissed: kotlin.Function0, onNotAllowGuestsClicked: kotlin.Function0, onAllowGuestsClicked: kotlin.Function0, onErrorDismissed: kotlin.Function0, onEditParticipantsClick: kotlin.Function0, onDiscardGroupCreationClick: kotlin.Function0, onRetryPendingCreation: kotlin.Function0, onBackPressed: kotlin.Function0, channelsHistoryOptionsEnabled: kotlin.Boolean, mlsReadReceiptsEnabled: kotlin.Boolean): kotlin.Unit skippable: false restartable: true params: @@ -9263,6 +9281,7 @@ private fun com.wire.android.ui.home.newconversation.groupOptions.GroupOptionScr - onErrorDismissed: STABLE (function type) - onEditParticipantsClick: STABLE (function type) - onDiscardGroupCreationClick: STABLE (function type) + - onRetryPendingCreation: STABLE (function type) - onBackPressed: STABLE (function type) - channelsHistoryOptionsEnabled: STABLE (primitive type) - mlsReadReceiptsEnabled: STABLE (primitive type) diff --git a/kalium b/kalium index b5017242483..b404171d70f 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b5017242483368a938f41896587d11aa75bb31ea +Subproject commit b404171d70f239fadc5f6656bf1f7adcd2e48c6b