Skip to content
Open
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ if (!project.hasProperty("skip.aboutlibraries")) {

dependencies {
implementation("com.wire.kalium:kalium-logic")
implementation("com.wire.kalium:kalium-network")
implementation("com.wire.kalium:kalium-util")
implementation("com.wire.kalium:kalium-cells")
implementation("com.wire.kalium:kalium-core-libsodium")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package com.wire.android.ui.home.conversations.details

import androidx.lifecycle.viewModelScope
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.ui.common.ActionsViewModel
import com.wire.android.ui.home.conversations.details.options.GroupConversationOptionsState
Expand All @@ -31,23 +32,31 @@
import com.wire.android.util.AppsUtil
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.UIText
import com.wire.kalium.common.error.CoreFailure
import com.wire.kalium.common.error.NetworkFailure
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.conversation.ConversationDetails
import com.wire.kalium.logic.data.featureConfig.Status
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.type.isExternal
import com.wire.kalium.logic.data.user.type.isFederated
import com.wire.kalium.logic.data.user.type.isRegularTeamMember
import com.wire.kalium.logic.data.user.type.isTeamAdmin
import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase
import com.wire.kalium.logic.feature.conversation.ConversationUpdateReceiptModeResult
import com.wire.kalium.logic.feature.conversation.MigrateConversationToMLSUseCase
import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationReceiptModeUseCase
import com.wire.kalium.logic.feature.debug.GetFeatureConfigResult
import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase
import com.wire.kalium.logic.feature.featureConfig.AppsAllowedResult
import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase
import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase
import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase
import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase
import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase
import com.wire.kalium.network.exceptions.KaliumException
import com.wire.kalium.util.DebugKaliumApi
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
Expand All @@ -66,6 +75,7 @@
import com.wire.android.di.metro.WireAssistedViewModelBinding
import com.wire.android.ui.home.conversations.ConversationDetailsManualViewModelFactoryGroup

@OptIn(DebugKaliumApi::class)
@Suppress("TooManyFunctions", "LongParameterList")
@WireAssistedViewModelBinding(ConversationDetailsManualViewModelFactoryGroup::class)
class GroupConversationDetailsViewModel @AssistedInject constructor(
Expand All @@ -80,6 +90,8 @@
private val isMLSEnabled: IsMLSEnabledUseCase,
refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase,
private val isWireCellsEnabled: IsWireCellsEnabledUseCase,
private val getFeatureConfig: GetFeatureConfigUseCase,
private val migrateConversationToMLS: MigrateConversationToMLSUseCase,
) : ActionsViewModel<GroupConversationDetailsViewAction>(),
GroupConversationParticipantsManager by GroupConversationParticipantsManagerImpl(
conversationId = navigationArgs.conversationId,
Expand All @@ -98,11 +110,33 @@

private val _isFetchingInitialData: MutableStateFlow<Boolean> = MutableStateFlow(true)
val isFetchingInitialData: MutableStateFlow<Boolean> = _isFetchingInitialData
private val isMlsMigrationEnabled = MutableStateFlow(false)
private var protocolTapCount = 0

init {
loadMlsMigrationFeatureFlag()
observeConversationDetails()
}

private fun loadMlsMigrationFeatureFlag() {
viewModelScope.launch {
isMlsMigrationEnabled.value = when (val result = withContext(dispatcher.io()) { getFeatureConfig() }) {
is GetFeatureConfigResult.Success -> {
val migrationConfig = result.featureConfigModel.mlsMigrationModel
migrationConfig?.status == Status.ENABLED
}

is GetFeatureConfigResult.Failure -> {
appLogger.w(

Check warning on line 130 in app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt#L129-L130

Added lines #L129 - L130 were not covered by tests
"[$TAG] Failed to load the manual MLS migration feature config " +
"(failureType=${result.coreFailure::class.simpleName})"

Check warning on line 132 in app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt#L132

Added line #L132 was not covered by tests
)
false

Check warning on line 134 in app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt#L134

Added line #L134 was not covered by tests
}
}
}
}

private suspend fun groupDetailsFlow(): Flow<ConversationDetails.Group> = observeConversationDetails(conversationId)
.filterIsInstance<ObserveConversationDetailsUseCase.Result.Success>()
.map { it.conversationDetails }
Expand Down Expand Up @@ -131,7 +165,8 @@
selfWithTeamFlow,
appsAllowedResultFlow,
observeSelfDeletionTimerSettingsForConversation(conversationId, considerSelfUserSettings = false),
) { groupDetails, (selfUser, selfTeam), appsAllowedResult, selfDeletionTimer ->
isMlsMigrationEnabled,
) { groupDetails, (selfUser, selfTeam), appsAllowedResult, selfDeletionTimer, isMlsMigrationEnabled ->
val selfType = selfUser.userType
val isSelfInTeamThatOwnsConversation = selfTeam?.id != null && selfTeam.id == groupDetails.conversation.teamId?.value
val isSelfExternalMember = selfUser.userType.isExternal()
Expand All @@ -141,6 +176,10 @@
val canPerformChannelAdminTasks = isChannel && isSelfInTeamThatOwnsConversation && isSelfTeamAdmin
val isRegularGroupAdmin = groupDetails.selfRole == Conversation.Member.Role.Admin
val canSelfPerformAdminTasks = (isRegularGroupAdmin) || (canPerformChannelAdminTasks)
val isConversationInSelfTeam = groupDetails.conversation.teamId?.let { it == selfUser.teamId } == true
val canManuallyMigrateToMLS = isMlsMigrationEnabled &&
groupDetails.conversation.protocol.isProteusOrMixed() &&
isConversationInSelfTeam
val channelPermissionType = groupDetails.getChannelPermissionType()
val channelAccessType = groupDetails.getChannelAccessType()
val isExternalOrFederated =
Expand Down Expand Up @@ -170,6 +209,10 @@
val mlsEnabled = isMLSEnabled()
val wireCellFeatureEnabled = isWireCellsEnabled()

if (!canManuallyMigrateToMLS) {
protocolTapCount = 0
}

updateState(
groupOptionsState.value.copy(
groupName = groupDetails.conversation.name.orEmpty(),
Expand Down Expand Up @@ -202,7 +245,10 @@
isWireCellEnabled = groupDetails.wireCell != null,
isWireCellFeatureEnabled = wireCellFeatureEnabled,
isSelfPartOfATeam = selfTeam != null,
canSelfAddParticipants = canSelfAddParticipants
canSelfAddParticipants = canSelfAddParticipants,
canManuallyMigrateToMLS = canManuallyMigrateToMLS,
shouldShowMlsMigrationDialog = groupOptionsState.value.shouldShowMlsMigrationDialog &&
canManuallyMigrateToMLS,

Check warning on line 251 in app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt#L251

Added line #L251 was not covered by tests
)
)
}.collect {}
Expand Down Expand Up @@ -263,6 +309,74 @@
updateReadReceiptRemoteRequest(enableReadReceipt)
}

fun onProtocolTapped() {
val state = groupOptionsState.value
if (!state.canManuallyMigrateToMLS) {
protocolTapCount = 0
return
}

protocolTapCount += 1
if (protocolTapCount == MANUAL_MIGRATION_TAP_COUNT) {
protocolTapCount = 0
appLogger.i("[$TAG] Manual MLS migration confirmation shown")
updateState(groupOptionsState.value.copy(shouldShowMlsMigrationDialog = true))
}
}

fun onMlsMigrationDialogDismissed() {
if (groupOptionsState.value.isMigratingToMLS) {
return
}
protocolTapCount = 0
appLogger.i("[$TAG] Manual MLS migration confirmation dismissed")
updateState(groupOptionsState.value.copy(shouldShowMlsMigrationDialog = false))

Check warning on line 333 in app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt#L331-L333

Added lines #L331 - L333 were not covered by tests
}

fun onMlsMigrationConfirmed() {
if (!groupOptionsState.value.canManuallyMigrateToMLS || groupOptionsState.value.isMigratingToMLS) {
return
}

appLogger.i(
"[$TAG] Manual MLS migration started " +
"(protocol=${groupOptionsState.value.protocolInfo.debugName()})"
)
updateState(groupOptionsState.value.copy(isMigratingToMLS = true))
viewModelScope.launch {
when (val result = migrateConversationToMLS(conversationId)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to follow the calls inside the MigrateConversationToMLSUseCase and it seems like not all of the branches with IO operations are executed in IO dispatcher.
I would recommend switching to IO dispatcher in the use case to call it safely from view model (main thread safety).

MigrateConversationToMLSUseCase.Result.Success -> {
appLogger.i("[$TAG] Manual MLS migration succeeded")
updateState(
groupOptionsState.value.copy(
shouldShowMlsMigrationDialog = false,
isMigratingToMLS = false,
)
)
sendAction(GroupConversationDetailsViewAction.Message(UIText.StringResource(R.string.mls_migration_success)))
}

is MigrateConversationToMLSUseCase.Result.Failure -> {
val backendError = result.cause.backendErrorDetails()
val backendErrorLog = backendError?.let {
", httpCode=${it.httpCode}, backendLabel=${it.label}"
}.orEmpty()
appLogger.e(
"[$TAG] Manual MLS migration failed " +
"(failureType=${result.cause::class.simpleName}$backendErrorLog)"
)
updateState(
groupOptionsState.value.copy(
shouldShowMlsMigrationDialog = false,
isMigratingToMLS = false,
)
)
sendAction(GroupConversationDetailsViewAction.Message(result.cause.mlsMigrationFailureText()))
}
}
}
}

private fun updateReadReceiptRemoteRequest(enableReadReceipt: Boolean) {
viewModelScope.launch {
val result = withContext(dispatcher.io()) {
Expand Down Expand Up @@ -296,9 +410,35 @@

companion object {
const val TAG = "GroupConversationDetailsViewModel"
private const val MANUAL_MIGRATION_TAP_COUNT = 5
}
}

private fun Conversation.ProtocolInfo.isProteusOrMixed(): Boolean =
this is Conversation.ProtocolInfo.Proteus || this is Conversation.ProtocolInfo.Mixed

private fun Conversation.ProtocolInfo.debugName(): String = when (this) {
is Conversation.ProtocolInfo.MLS -> "MLS"
is Conversation.ProtocolInfo.Mixed -> "Mixed"
Conversation.ProtocolInfo.Proteus -> "Proteus"
}

private fun CoreFailure.mlsMigrationFailureText(): UIText = backendErrorDetails()?.let {
UIText.StringResource(R.string.mls_migration_failure_with_backend_error, it.httpCode, it.label)
} ?: UIText.StringResource(R.string.mls_migration_failure)

private fun CoreFailure.backendErrorDetails(): BackendErrorDetails? =
(this as? NetworkFailure.ServerMiscommunication)?.kaliumException?.let { exception ->
when (exception) {
is KaliumException.RedirectError -> exception.errorResponse
is KaliumException.InvalidRequestError -> exception.errorResponse
is KaliumException.ServerError -> exception.errorResponse
else -> null

Check warning on line 436 in app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt#L436

Added line #L436 was not covered by tests
}
}?.let { BackendErrorDetails(it.code, it.label) }

private data class BackendErrorDetails(val httpCode: Int, val label: String)

sealed interface GroupConversationDetailsViewAction {
data class Message(val text: UIText) : GroupConversationDetailsViewAction
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ import androidx.compose.ui.unit.times
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.wire.android.BuildConfig
import com.wire.android.R
import com.wire.android.model.ClickBlockParams
import com.wire.android.model.Clickable
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.dimensions
import com.wire.android.ui.common.rowitem.SectionHeader
import com.wire.android.ui.common.wireDialogPropertiesBuilder
import com.wire.android.ui.home.conversations.details.GroupConversationDetailsViewModel
import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration
import com.wire.android.ui.home.newconversation.channelaccess.ChannelAccessType
Expand Down Expand Up @@ -84,7 +87,16 @@ fun GroupConversationOptions(
onReadReceiptSwitchClicked = viewModel::onReadReceiptUpdate,
lazyListState = lazyListState,
onEditGroupName = onEditGroupName,
onProtocolClicked = viewModel::onProtocolTapped,
)

if (state.shouldShowMlsMigrationDialog) {
MlsMigrationConfirmationDialog(
isLoading = state.isMigratingToMLS,
onConfirm = viewModel::onMlsMigrationConfirmed,
onDismiss = viewModel::onMlsMigrationDialogDismissed,
)
}
}

@Composable
Expand All @@ -96,6 +108,7 @@ fun GroupConversationSettings(
onSelfDeletingClicked: () -> Unit,
onReadReceiptSwitchClicked: (Boolean) -> Unit,
onEditGroupName: () -> Unit,
onProtocolClicked: () -> Unit,
modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(),
mlsReadReceiptsEnabled: Boolean = BuildConfig.MLS_READ_RECEIPTS_ENABLED,
Expand Down Expand Up @@ -219,7 +232,10 @@ fun GroupConversationSettings(

folderWithItems(
folderTitleResId = R.string.folder_label_protocol_details,
items = conversationProtocolDetailsItems(protocolInfo = state.protocolInfo),
items = conversationProtocolDetailsItems(
protocolInfo = state.protocolInfo,
onProtocolClicked = onProtocolClicked,
),
)
}
}
Expand Down Expand Up @@ -258,11 +274,16 @@ private fun <E> MutableList<E>.addIf(condition: Boolean, element: E) {

private fun conversationProtocolDetailsItems(
protocolInfo: Conversation.ProtocolInfo,
onProtocolClicked: () -> Unit,
): List<@Composable () -> Unit> = buildList {
add {
ProtocolDetails(
label = UIText.StringResource(R.string.protocol),
text = UIText.DynamicString(protocolInfo.name())
text = UIText.DynamicString(protocolInfo.name()),
clickable = Clickable(
clickBlockParams = ClickBlockParams(debounceClicks = false),
onClick = onProtocolClicked,
),
)
}

Expand Down Expand Up @@ -315,15 +336,45 @@ private fun GroupNameItem(
}

@Composable
private fun ProtocolDetails(label: UIText, text: UIText) {
private fun ProtocolDetails(label: UIText, text: UIText, clickable: Clickable = Clickable(enabled = false)) {
GroupConversationOptionsItem(
label = label.asString(),
title = text.asString(),
arrowType = ArrowType.NONE
arrowType = ArrowType.NONE,
clickable = clickable,
)
HorizontalDivider(thickness = Dp.Hairline, color = MaterialTheme.wireColorScheme.divider)
}

@Composable
private fun MlsMigrationConfirmationDialog(
isLoading: Boolean,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
WireDialog(
title = stringResource(R.string.mls_migration_dialog_title),
text = stringResource(R.string.mls_migration_dialog_description),
onDismiss = onDismiss,
buttonsHorizontalAlignment = false,
properties = wireDialogPropertiesBuilder(
dismissOnBackPress = !isLoading,
dismissOnClickOutside = false,
),
optionButton1Properties = WireDialogButtonProperties(
onClick = onConfirm,
text = stringResource(R.string.mls_migration_dialog_confirm),
type = WireDialogButtonType.Primary,
loading = isLoading,
),
dismissButtonProperties = WireDialogButtonProperties(
onClick = onDismiss,
text = stringResource(R.string.label_cancel),
state = if (isLoading) WireButtonState.Disabled else WireButtonState.Default,
),
)
}

@Composable
private fun ReadReceiptOption(
isSwitchEnabled: Boolean,
Expand Down Expand Up @@ -421,6 +472,7 @@ private fun PreviewGroupConversationOptions(state: GroupConversationOptionsState
onAppsAccessItemClicked = {},
onReadReceiptSwitchClicked = {},
onEditGroupName = {},
onProtocolClicked = {},
modifier = Modifier,
lazyListState = rememberLazyListState(),
mlsReadReceiptsEnabled = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ data class GroupConversationOptionsState(
val isWireCellFeatureEnabled: Boolean = false,
val isWireCellEnabled: Boolean = false,
val isSelfPartOfATeam: Boolean = false,
val canSelfAddParticipants: Boolean = false
val canSelfAddParticipants: Boolean = false,
val canManuallyMigrateToMLS: Boolean = false,
val shouldShowMlsMigrationDialog: Boolean = false,
val isMigratingToMLS: Boolean = false,
) {

sealed interface Error {
Expand Down
Loading
Loading