Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ 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
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
Expand All @@ -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,
Expand All @@ -67,6 +71,8 @@ class ConversationInfoViewModel @AssistedInject constructor(

var conversationInfoViewState by mutableStateOf(ConversationInfoViewState(conversationId))

private var pendingMLSCreationRetryStarted = false

init {
fetchMLSVerificationStatus()
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
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
Expand Down Expand Up @@ -69,6 +70,20 @@
private val observeIsAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase
) : ViewModel() {

private data class GroupCreationAttempt(
val name: String,
val userIdList: List<UserId>,
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())

Expand Down Expand Up @@ -113,6 +128,7 @@
fun resetState() {
newGroupNameTextState.clearText()
newGroupState = GroupMetadataState()
pendingMLSGroupCreation = null
loadDefaultProtocol()
observeAllowanceOfAppsUsageInitialState()
createGroupState = CreateGroupState.Default
Expand Down Expand Up @@ -189,6 +205,25 @@
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)
}
Expand Down Expand Up @@ -258,7 +293,7 @@
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(
Expand All @@ -273,34 +308,38 @@
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(
protocol = CreateConversationParam.Protocol.PROTEUS,
accessRole = Conversation.defaultGroupAccessRoles,
access = Conversation.defaultGroupAccess,
wireCellEnabled = groupOptionsState.isWireCellsEnabled ?: false,
)
),
isChannel = false,
)
handleNewGroupCreationResult(result)
val result = createOrRetryGroup(attempt)
handleNewGroupCreationResult(result, attempt)
}
}

private fun createGroupForTeamAccounts(shouldCheckGuests: Boolean = true) {
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) },
Expand All @@ -314,19 +353,41 @@
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)

Check warning on line 367 in app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt#L367

Added line #L367 was not covered by tests
} 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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(
Expand Down Expand Up @@ -102,26 +117,66 @@ 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, {}, {}, {}, {}, {})
}
}

@PreviewMultipleThemes
@Composable
private fun PreviewCreateGroupErrorDialogUnknown() {
WireTheme {
CreateGroupErrorDialog(CreateGroupState.Error.Unknown, {}, {}, {})
CreateGroupErrorDialog(CreateGroupState.Error.Unknown, {}, {}, {}, {}, {})
}
}

@PreviewMultipleThemes
@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(), {}, {}, {}, {}, {})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) : Error

val isConflictedBackends get() = this is ConflictedBackends
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fun GroupOptionScreen(
newConversationViewModel.onCreateGroupErrorDismiss()
navigator.navigate(NavigationCommand(HomeScreenDestination, BackStackMode.CLEAR_WHOLE))
},
onRetryPendingCreation = newConversationViewModel::retryPendingMLSGroupCreation,
onErrorDismissed = newConversationViewModel::onCreateGroupErrorDismiss,
onEnableWireCellChanged = newConversationViewModel::onEnableWireCellChanged
)
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -465,6 +474,7 @@ private fun PreviewGroupOptionScreen(
onErrorDismissed = {},
onEditParticipantsClick = {},
onDiscardGroupCreationClick = {},
onRetryPendingCreation = {},
onBackPressed = {},
channelsHistoryOptionsEnabled = channelsHistoryOptionsEnabled,
mlsReadReceiptsEnabled = mlsReadReceiptsEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading
Loading