diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index b13990b3a8..98ec65abe7 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -219,6 +219,11 @@ class UseCaseModule { fun provideUpdateApiVersionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = coreLogic.getGlobalScope().updateApiVersions + @ViewModelScoped + @Provides + fun provideDisableEventProcessing(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = + coreLogic.getSessionScope(currentAccount).debug.disableEventProcessing + @ViewModelScoped @Provides fun provideCurrentSessionFlowUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = diff --git a/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt index 676cb93a7f..a85a042508 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt @@ -60,6 +60,7 @@ class SystemMessageContentMapper @Inject constructor( is MessageContent.NewConversationReceiptMode -> mapNewConversationReceiptMode(content) is MessageContent.ConversationReceiptModeChanged -> mapConversationReceiptModeChanged(message.senderUserId, content, members) is MessageContent.HistoryLost -> mapConversationHistoryLost() + is MessageContent.HistoryLostProtocolChanged -> mapConversationHistoryListProtocolChanged() is MessageContent.ConversationMessageTimerChanged -> mapConversationTimerChanged(message.senderUserId, content, members) is MessageContent.ConversationCreated -> mapConversationCreated(message.senderUserId, message.date, members) is MessageContent.MLSWrongEpochWarning -> mapMLSWrongEpochWarning() @@ -68,6 +69,7 @@ class SystemMessageContentMapper @Inject constructor( is MessageContent.ConversationVerifiedMLS -> mapConversationVerified(Conversation.Protocol.MLS) is MessageContent.ConversationVerifiedProteus -> mapConversationVerified(Conversation.Protocol.PROTEUS) is MessageContent.FederationStopped -> mapFederationMessage(content) + is MessageContent.ConversationProtocolChanged -> mapConversationProtocolChanged(content) } private fun mapConversationCreated(senderUserId: UserId, date: String, userList: List): UIMessageContent.SystemMessage { @@ -108,6 +110,12 @@ class SystemMessageContentMapper @Inject constructor( } } + private fun mapConversationProtocolChanged( + content: MessageContent.ConversationProtocolChanged + ): UIMessageContent.SystemMessage { + return UIMessageContent.SystemMessage.ConversationProtocolChanged(content.protocol) + } + private fun mapResetSession( senderUserId: UserId, userList: List @@ -236,11 +244,14 @@ class SystemMessageContentMapper @Inject constructor( is MessageContent.FederationStopped.Removed -> UIMessageContent.SystemMessage.FederationStopped(listOf(content.domain)) } - private fun mapConversationHistoryLost(): UIMessageContent.SystemMessage = UIMessageContent.SystemMessage.HistoryLost() - private fun mapMLSWrongEpochWarning(): UIMessageContent.SystemMessage = UIMessageContent.SystemMessage.MLSWrongEpochWarning() + private fun mapConversationHistoryLost(): UIMessageContent.SystemMessage = + UIMessageContent.SystemMessage.HistoryLost + private fun mapMLSWrongEpochWarning(): UIMessageContent.SystemMessage = + UIMessageContent.SystemMessage.MLSWrongEpochWarning() + private fun mapConversationHistoryListProtocolChanged(): UIMessageContent.SystemMessage = + UIMessageContent.SystemMessage.HistoryLostProtocolChanged private fun mapConversationDegraded(protocol: Conversation.Protocol): UIMessageContent.SystemMessage = UIMessageContent.SystemMessage.ConversationDegraded(protocol) - private fun mapConversationVerified(protocol: Conversation.Protocol): UIMessageContent.SystemMessage = UIMessageContent.SystemMessage.ConversationVerified(protocol) diff --git a/app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt index c9f8ebac10..9672609feb 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt @@ -57,6 +57,7 @@ class UIParticipantMapper @Inject constructor( botService = (user as? OtherUser)?.botService, isDefederated = (user is OtherUser && user.defederated), isProteusVerified = (user is OtherUser && user.isProteusVerified), + supportedProtocolList = supportedProtocols.orEmpty().toList() ) } @@ -71,7 +72,8 @@ class UIParticipantMapper @Inject constructor( isDeleted = userSummary.isUserDeleted, isSelf = isSelfUser, isDefederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocolList = listOf() ) } @@ -87,7 +89,8 @@ class UIParticipantMapper @Inject constructor( isSelf = false, readReceiptDate = date, isDefederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocolList = listOf() ) } diff --git a/app/src/main/kotlin/com/wire/android/migration/MigrationMapper.kt b/app/src/main/kotlin/com/wire/android/migration/MigrationMapper.kt index 8ddc983918..7803ed9edc 100644 --- a/app/src/main/kotlin/com/wire/android/migration/MigrationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/migration/MigrationMapper.kt @@ -39,6 +39,7 @@ import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.SsoId +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType @@ -203,7 +204,8 @@ class MigrationMapper @Inject constructor() { connectionStatus = ConnectionState.ACCEPTED, previewPicture = scalaUserData.pictureAssetId?.let { toQualifiedId(it, scalaUserData.domain, selfuser) }, completePicture = scalaUserData.pictureAssetId?.let { toQualifiedId(it, scalaUserData.domain, selfuser) }, - availabilityStatus = mapUserAvailabilityStatus(scalaUserData.availability) + availabilityStatus = mapUserAvailabilityStatus(scalaUserData.availability), + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) } else { val botService = @@ -232,7 +234,8 @@ class MigrationMapper @Inject constructor() { botService = botService, deleted = scalaUserData.deleted, defederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/DeletedLabel.kt b/app/src/main/kotlin/com/wire/android/ui/common/StatusBox.kt similarity index 71% rename from app/src/main/kotlin/com/wire/android/ui/common/DeletedLabel.kt rename to app/src/main/kotlin/com/wire/android/ui/common/StatusBox.kt index c0ee115f25..b23d03cc49 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/DeletedLabel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/StatusBox.kt @@ -32,11 +32,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.wire.android.R import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.util.ui.PreviewMultipleThemes /** * Outlined box with a text inside. @@ -46,17 +47,24 @@ import com.wire.android.ui.theme.wireColorScheme @Composable fun StatusBox( statusText: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + textColor: Color = MaterialTheme.wireColorScheme.labelText, + badgeColor: Color = MaterialTheme.wireColorScheme.surface, + withBorder: Boolean = true, ) { Box( modifier = modifier .wrapContentSize() .clip(RoundedCornerShape(size = dimensions().spacing4x)) - .background(colorsScheme().surface) + .background(badgeColor) .border( BorderStroke( width = 1.dp, - color = MaterialTheme.wireColorScheme.divider + color = if (withBorder) { + MaterialTheme.wireColorScheme.divider + } else { + badgeColor + } ), shape = RoundedCornerShape(size = dimensions().spacing4x), ) @@ -68,7 +76,7 @@ fun StatusBox( ) { Text( text = statusText, - style = typography().label03.copy(color = MaterialTheme.wireColorScheme.labelText) + style = typography().label03.copy(color = textColor) ) } } @@ -81,8 +89,28 @@ fun DeletedLabel(modifier: Modifier = Modifier) { ) } -@Preview +@Composable +fun ProtocolLabel( + protocolName: String, + modifier: Modifier = Modifier +) { + StatusBox( + statusText = protocolName, + modifier = modifier, + textColor = MaterialTheme.wireColorScheme.onPrimary, + badgeColor = MaterialTheme.wireColorScheme.primary, + withBorder = false + ) +} + +@PreviewMultipleThemes @Composable fun PreviewDeletedLabel() { DeletedLabel() } + +@PreviewMultipleThemes +@Composable +fun PreviewProtocolLabel() { + ProtocolLabel("MLS") +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt index 471c21f20c..343cad91d1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt @@ -45,7 +45,6 @@ import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversationslist.common.FolderHeader import com.wire.android.ui.home.settings.SettingsItem -import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography @@ -53,6 +52,7 @@ import com.wire.android.util.getDeviceIdString import com.wire.android.util.getGitBuildId import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.debug.DisableEventProcessingUseCase import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountResult import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler @@ -66,6 +66,7 @@ import javax.inject.Inject //region DebugDataOptionsViewModel data class DebugDataOptionsState( val isEncryptedProteusStorageEnabled: Boolean = false, + val isEventProcessingDisabled: Boolean = false, val keyPackagesCount: Int = 0, val mslClientId: String = "null", val mlsErrorMessage: String = "null", @@ -83,7 +84,8 @@ class DebugDataOptionsViewModel private val globalDataStore: GlobalDataStore, private val updateApiVersions: UpdateApiVersionsScheduler, private val mlsKeyPackageCountUseCase: MLSKeyPackageCountUseCase, - private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase + private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase, + private val disableEventProcessingUseCase: DisableEventProcessingUseCase ) : ViewModel() { var state by mutableStateOf( @@ -118,6 +120,13 @@ class DebugDataOptionsViewModel updateApiVersions.scheduleImmediateApiVersionUpdate() } + fun disableEventProcessing(disabled: Boolean) { + viewModelScope.launch { + disableEventProcessingUseCase(disabled) + state = state.copy(isEventProcessingDisabled = disabled) + } + } + //region Private private fun observeEncryptedProteusStorageState() { viewModelScope.launch { @@ -189,7 +198,8 @@ fun DebugDataOptions( onEnableEncryptedProteusStorageChange = viewModel::enableEncryptedProteusStorage, onRestartSlowSyncForRecovery = viewModel::restartSlowSyncForRecovery, onForceUpdateApiVersions = viewModel::forceUpdateApiVersions, - onManualMigrationPressed = { onManualMigrationPressed(viewModel.currentAccount) } + onManualMigrationPressed = { onManualMigrationPressed(viewModel.currentAccount) }, + onDisableEventProcessingChange = viewModel::disableEventProcessing ) } @@ -201,6 +211,7 @@ fun DebugDataOptionsContent( buildVariant: String, onCopyText: (String) -> Unit, onEnableEncryptedProteusStorageChange: (Boolean) -> Unit, + onDisableEventProcessingChange: (Boolean) -> Unit, onRestartSlowSyncForRecovery: () -> Unit, onForceUpdateApiVersions: () -> Unit, onManualMigrationPressed: () -> Unit @@ -264,7 +275,12 @@ fun DebugDataOptionsContent( onCopyText = onCopyText ) - DevelopmentApiVersioningOptions(onForceLatestDevelopmentApiChange = onForceUpdateApiVersions) + DebugToolsOptions( + isEventProcessingEnabled = state.isEventProcessingDisabled, + onDisableEventProcessingChange = onDisableEventProcessingChange, + onRestartSlowSyncForRecovery = onRestartSlowSyncForRecovery, + onForceUpdateApiVersions = onForceUpdateApiVersions + ) } if (state.isManualMigrationAllowed) { @@ -304,35 +320,6 @@ private fun ManualMigrationOptions( } //endregion -//region Development API Options -@Composable -private fun DevelopmentApiVersioningOptions( - onForceLatestDevelopmentApiChange: () -> Unit -) { - FolderHeader(stringResource(R.string.debug_settings_api_versioning_title)) - RowItemTemplate( - modifier = Modifier.wrapContentWidth(), - title = { - Text( - style = MaterialTheme.wireTypography.body01, - color = MaterialTheme.wireColorScheme.onBackground, - text = stringResource(R.string.debug_settings_force_api_versioning_update), - modifier = Modifier.padding(start = dimensions().spacing8x) - ) - }, - actions = { - WirePrimaryButton( - minSize = MaterialTheme.wireDimensions.buttonMediumMinSize, - minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, - onClick = onForceLatestDevelopmentApiChange, - text = stringResource(R.string.debug_settings_force_api_versioning_update_button_text), - fillMaxWidth = false - ) - } - ) -} -//endregion - //region MLS Options @Composable private fun MLSOptions( @@ -371,6 +358,68 @@ private fun MLSOptions( onClick = { onCopyText(mlsClientId) } ) ) + } +} +//endregion + +//region Proteus Options +@Composable +private fun ProteusOptions( + isEncryptedStorageEnabled: Boolean, + onEncryptedStorageEnabledChange: (Boolean) -> Unit, +) { + FolderHeader(stringResource(R.string.label_proteus_option_title)) + EnableEncryptedProteusStorageSwitch( + isEnabled = isEncryptedStorageEnabled, + onCheckedChange = onEncryptedStorageEnabledChange + ) +} + +@Composable +private fun EnableEncryptedProteusStorageSwitch( + isEnabled: Boolean = false, + onCheckedChange: ((Boolean) -> Unit)?, +) { + RowItemTemplate( + title = { + Text( + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.onBackground, + text = stringResource(R.string.label_enable_encrypted_proteus_storage), + modifier = Modifier.padding(start = dimensions().spacing8x) + ) + }, + actions = { + WireSwitch( + checked = isEnabled, + onCheckedChange = onCheckedChange, + enabled = !isEnabled, + modifier = Modifier + .padding(end = dimensions().spacing8x) + .size( + width = dimensions().buttonSmallMinSize.width, + height = dimensions().buttonSmallMinSize.height + ) + ) + } + ) +} +//endregion + +//region Debug Tools +@Composable +private fun DebugToolsOptions( + isEventProcessingEnabled: Boolean, + onDisableEventProcessingChange: (Boolean) -> Unit, + onRestartSlowSyncForRecovery: () -> Unit, + onForceUpdateApiVersions: () -> Unit +) { + FolderHeader(stringResource(R.string.label_debug_tools_title)) + Column { + DisableEventProcessingSwitch( + isEnabled = isEventProcessingEnabled, + onCheckedChange = onDisableEventProcessingChange + ) RowItemTemplate( modifier = Modifier.wrapContentWidth(), title = { @@ -385,31 +434,37 @@ private fun MLSOptions( WirePrimaryButton( minSize = MaterialTheme.wireDimensions.buttonMediumMinSize, minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, - onClick = restartSlowSyncForRecovery, + onClick = onRestartSlowSyncForRecovery, text = stringResource(R.string.restart_slowsync_for_recovery_button), fillMaxWidth = false ) } ) + RowItemTemplate( + modifier = Modifier.wrapContentWidth(), + title = { + Text( + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.onBackground, + text = stringResource(R.string.debug_settings_force_api_versioning_update), + modifier = Modifier.padding(start = dimensions().spacing8x) + ) + }, + actions = { + WirePrimaryButton( + minSize = MaterialTheme.wireDimensions.buttonMediumMinSize, + minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, + onClick = onForceUpdateApiVersions, + text = stringResource(R.string.debug_settings_force_api_versioning_update_button_text), + fillMaxWidth = false + ) + } + ) } } -//endregion - -//region Proteus Options -@Composable -private fun ProteusOptions( - isEncryptedStorageEnabled: Boolean, - onEncryptedStorageEnabledChange: (Boolean) -> Unit, -) { - FolderHeader(stringResource(R.string.label_proteus_option_title)) - EnableEncryptedProteusStorageSwitch( - isEnabled = isEncryptedStorageEnabled, - onCheckedChange = onEncryptedStorageEnabledChange - ) -} @Composable -private fun EnableEncryptedProteusStorageSwitch( +private fun DisableEventProcessingSwitch( isEnabled: Boolean = false, onCheckedChange: ((Boolean) -> Unit)?, ) { @@ -418,7 +473,7 @@ private fun EnableEncryptedProteusStorageSwitch( Text( style = MaterialTheme.wireTypography.body01, color = MaterialTheme.wireColorScheme.onBackground, - text = stringResource(R.string.label_enable_encrypted_proteus_storage), + text = stringResource(R.string.label_disable_event_processing), modifier = Modifier.padding(start = dimensions().spacing8x) ) }, @@ -426,7 +481,6 @@ private fun EnableEncryptedProteusStorageSwitch( WireSwitch( checked = isEnabled, onCheckedChange = onCheckedChange, - enabled = !isEnabled, modifier = Modifier .padding(end = dimensions().spacing8x) .size( @@ -457,17 +511,8 @@ fun PreviewOtherDebugOptions() { ), onEnableEncryptedProteusStorageChange = {}, onForceUpdateApiVersions = {}, + onDisableEventProcessingChange = {}, onRestartSlowSyncForRecovery = {}, onManualMigrationPressed = {} ) } - -@PreviewMultipleThemes -@Composable -fun PreviewDevelopmentApiVersioningOptions() { - WireTheme { - DevelopmentApiVersioningOptions( - onForceLatestDevelopmentApiChange = {} - ) - } -} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 8ef9322ebd..f83d09ed20 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -64,6 +64,7 @@ import com.wire.android.R import com.wire.android.appLogger import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.SnackBarMessage +import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.calling.common.MicrophoneBTPermissionsDeniedDialog @@ -77,6 +78,7 @@ import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog import com.wire.android.ui.common.dialogs.calling.OngoingActiveCallDialog import com.wire.android.ui.common.error.CoreFailureErrorDialog import com.wire.android.ui.common.snackbar.LocalSnackbarHostState +import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.GroupConversationDetailsScreenDestination import com.wire.android.ui.destinations.InitiatingCallScreenDestination import com.wire.android.ui.destinations.MediaGalleryScreenDestination @@ -97,6 +99,7 @@ import com.wire.android.ui.home.conversations.info.ConversationInfoViewModel import com.wire.android.ui.home.conversations.info.ConversationInfoViewState import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewModel import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewState +import com.wire.android.ui.home.conversations.migration.ConversationMigrationViewModel import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration @@ -154,6 +157,7 @@ fun ConversationScreen( conversationCallViewModel: ConversationCallViewModel = hiltViewModel(), conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel(), messageComposerViewModel: MessageComposerViewModel = hiltViewModel(), + conversationMigrationViewModel: ConversationMigrationViewModel = hiltViewModel(), groupDetailsScreenResultRecipient: ResultRecipient, mediaGalleryScreenResultRecipient: ResultRecipient, resultNavigator: ResultBackNavigator, @@ -180,6 +184,15 @@ fun ConversationScreen( } val context = LocalContext.current + conversationMigrationViewModel.migratedConversationId?.let { migratedConversationId -> + navigator.navigate( + NavigationCommand( + ConversationScreenDestination(migratedConversationId), + BackStackMode.REMOVE_CURRENT + ) + ) + } + with(conversationCallViewModel) { if (conversationCallViewState.shouldShowJoinAnywayDialog) { appLogger.i("showing showJoinAnywayDialog..") diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationTopAppBar.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationTopAppBar.kt index 790983d6a6..3b048f551e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationTopAppBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationTopAppBar.kt @@ -188,7 +188,7 @@ private fun VerificationIcon(protocolInfo: Conversation.ProtocolInfo?, verificat is Conversation.ProtocolInfo.MLS -> R.drawable.ic_certificate_valid_mls to R.string.content_description_mls_certificate_valid - Conversation.ProtocolInfo.Proteus -> + is Conversation.ProtocolInfo.Proteus, is Conversation.ProtocolInfo.Mixed -> R.drawable.ic_certificate_valid_proteus to R.string.content_description_proteus_certificate_valid } Image( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt index f87ad8437a..9266e57dc6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt @@ -256,7 +256,9 @@ private fun getColorFilter(message: SystemMessage): ColorFilter? { is SystemMessage.TeamMemberRemoved, is SystemMessage.ConversationReceiptModeChanged, is SystemMessage.HistoryLost, + is SystemMessage.HistoryLostProtocolChanged, is SystemMessage.NewConversationReceiptMode, + is SystemMessage.ConversationProtocolChanged, is SystemMessage.ConversationMessageTimerActivated, is SystemMessage.ConversationMessageCreated, is SystemMessage.ConversationStartedWithMembers, @@ -482,6 +484,8 @@ private val SystemMessage.expandable is SystemMessage.ConversationReceiptModeChanged -> false is SystemMessage.Knock -> false is SystemMessage.HistoryLost -> false + is SystemMessage.HistoryLostProtocolChanged -> false + is SystemMessage.ConversationProtocolChanged -> false is SystemMessage.ConversationMessageTimerActivated -> false is SystemMessage.ConversationMessageTimerDeactivated -> false is SystemMessage.ConversationMessageCreated -> false @@ -555,6 +559,8 @@ fun SystemMessage.annotatedString( is SystemMessage.MLSWrongEpochWarning -> arrayOf() is SystemMessage.ConversationDegraded -> arrayOf() is SystemMessage.ConversationVerified -> arrayOf() + is SystemMessage.HistoryLostProtocolChanged -> arrayOf() + is SystemMessage.ConversationProtocolChanged -> arrayOf() is SystemMessage.ConversationMessageTimerActivated -> arrayOf( author.asString(res), selfDeletionDuration.longLabel.asString(res) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt index 051ec0bf56..d99a9c080e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt @@ -43,7 +43,6 @@ import kotlinx.coroutines.launch import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) -@Suppress("LongParameterList") @HiltViewModel class ConversationBannerViewModel @Inject constructor( override val savedStateHandle: SavedStateHandle, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/ConversationParticipantItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/ConversationParticipantItem.kt index a4f603b8cd..5f1d458750 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/ConversationParticipantItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/ConversationParticipantItem.kt @@ -31,11 +31,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.ArrowRightIcon import com.wire.android.ui.common.ProteusVerifiedIcon +import com.wire.android.ui.common.ProtocolLabel import com.wire.android.ui.common.RowItemTemplate import com.wire.android.ui.common.UserBadge import com.wire.android.ui.common.UserProfileAvatar @@ -49,6 +51,7 @@ import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.EMPTY import com.wire.android.util.uiReadReceiptDateTime +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserId @Composable @@ -95,6 +98,14 @@ fun ConversationParticipantItem( ) if (uiParticipant.isProteusVerified) ProteusVerifiedIcon() + if (BuildConfig.MLS_SUPPORT_ENABLED && BuildConfig.DEVELOPER_FEATURES_ENABLED) { + uiParticipant.supportedProtocolList.map { + ProtocolLabel( + protocolName = it.name, + Modifier.padding(start = dimensions().spacing4x) + ) + } + } } }, subtitle = { @@ -127,7 +138,16 @@ fun ConversationParticipantItem( @Composable fun PreviewGroupConversationParticipantItem() { ConversationParticipantItem( - UIParticipant(UserId("0", ""), "name", "handle", false, false, UserAvatarData(), Membership.Guest, isProteusVerified = true), + UIParticipant( + UserId("0", ""), + "name", + "handle", + false, + false, + UserAvatarData(), + Membership.Guest, + isProteusVerified = true, + supportedProtocolList = listOf(SupportedProtocol.PROTEUS, SupportedProtocol.MLS)), clickable = Clickable(enabled = true) {} ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipants.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipants.kt index 8a0d1a98d3..86d4d43f57 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipants.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipants.kt @@ -23,34 +23,45 @@ package com.wire.android.ui.home.conversations.details.participants import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.style.TextAlign +import com.wire.android.BuildConfig import com.wire.android.R +import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.rememberBottomBarElevationState -import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.stringWithStyledArgs +import com.wire.kalium.logic.data.user.SupportedProtocol @Composable fun GroupConversationParticipants( openFullListPressed: () -> Unit, onProfilePressed: (UIParticipant) -> Unit, - onAddParticipantsPressed : () -> Unit, + onAddParticipantsPressed: () -> Unit, groupParticipantsState: GroupConversationParticipantsState, lazyListState: LazyListState = rememberLazyListState() ) { @@ -77,20 +88,32 @@ fun GroupConversationParticipants( groupParticipantsState.data.allCount.toString() ) ) - if (groupParticipantsState.data.isSelfAnAdmin) + if (groupParticipantsState.data.isSelfAnAdmin) { WirePrimaryButton( text = stringResource(R.string.conversation_details_group_participants_add), fillMaxWidth = true, - onClick = onAddParticipantsPressed , + onClick = onAddParticipantsPressed, modifier = Modifier .fillMaxWidth() .padding(top = MaterialTheme.wireDimensions.spacing16x), ) + } + if (BuildConfig.MLS_SUPPORT_ENABLED && BuildConfig.DEVELOPER_FEATURES_ENABLED) { + val groupParticipants = groupParticipantsState.data.allParticipants + MLSProgressIndicator( + progress = (groupParticipants) + .filter { it.supportedProtocolList.contains(SupportedProtocol.MLS) } + .size / (groupParticipantsState.data.allCount).toFloat(), + modifier = Modifier + .padding(top = dimensions().spacing16x) + .background(MaterialTheme.wireColorScheme.surface) + ) + } } } participantsFoldersWithElements(context, groupParticipantsState, onProfilePressed) } - if (groupParticipantsState.showAllVisible) + if (groupParticipantsState.showAllVisible) { Surface( shadowElevation = lazyListState.rememberBottomBarElevationState().value, color = MaterialTheme.wireColorScheme.background @@ -102,11 +125,50 @@ fun GroupConversationParticipants( ) } } + } } } -@Preview +@Composable +fun MLSProgressIndicator( + progress: Float, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.wireColorScheme.primary, + trackColor: Color = MaterialTheme.wireColorScheme.uncheckedColor +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(dimensions().spacing28x)) + .height(dimensions().spacing32x), + contentAlignment = Alignment.Center + ) { + LinearProgressIndicator( + progress = progress, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + color = color, + trackColor = trackColor + ) + Text( + style = MaterialTheme.typography.labelLarge, + text = "${SupportedProtocol.MLS.name} (${String.format("%.2f", progress * 100)}%)", + textAlign = TextAlign.Center, + color = MaterialTheme.wireColorScheme.onPrimary, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@PreviewMultipleThemes @Composable fun PreviewGroupConversationParticipants() { GroupConversationParticipants({}, {}, {}, GroupConversationParticipantsState.PREVIEW) } + +@PreviewMultipleThemes +@Composable +fun PreviewMLSProgressIndicator() { + MLSProgressIndicator(0.25F) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsState.kt index 7e398d734c..48d0197329 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsState.kt @@ -22,6 +22,7 @@ package com.wire.android.ui.home.conversations.details.participants import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserId data class GroupConversationParticipantsState( @@ -32,8 +33,24 @@ data class GroupConversationParticipantsState( companion object { val PREVIEW = GroupConversationParticipantsState( data = ConversationParticipantsData( - admins = listOf(UIParticipant(UserId("0", ""), "name", "handle", true)), - participants = listOf(UIParticipant(UserId("1", ""), "name", "handle", false)), + admins = listOf( + UIParticipant( + id = UserId("0", ""), + name = "admin", + handle = "handle", + isSelf = true, + supportedProtocolList = listOf(SupportedProtocol.MLS) + ) + ), + participants = listOf( + UIParticipant( + id = UserId("1", ""), + name = "participant", + handle = "handle", + isSelf = true, + supportedProtocolList = listOf(SupportedProtocol.PROTEUS) + ) + ), allAdminsCount = 1, allParticipantsCount = 1, isSelfAnAdmin = true diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/model/ConversationParticipantsData.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/model/ConversationParticipantsData.kt index 63be810fb4..7564bcf728 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/model/ConversationParticipantsData.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/model/ConversationParticipantsData.kt @@ -28,4 +28,5 @@ data class ConversationParticipantsData( val isSelfAnAdmin: Boolean = false ) { val allCount: Int = allAdminsCount + allParticipantsCount + val allParticipants: List = participants + admins } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/model/UIParticipant.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/model/UIParticipant.kt index eec25b9a54..0a12bb2ef1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/model/UIParticipant.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/model/UIParticipant.kt @@ -24,6 +24,7 @@ import com.wire.android.model.UserAvatarData import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.kalium.logic.data.user.BotService import com.wire.kalium.logic.data.user.ConnectionState +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserId import kotlinx.datetime.Instant @@ -41,5 +42,6 @@ data class UIParticipant( val readReceiptDate: Instant? = null, val botService: BotService? = null, val isDefederated: Boolean = false, - val isProteusVerified: Boolean = false + val isProteusVerified: Boolean = false, + val supportedProtocolList: List = listOf() ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt new file mode 100644 index 0000000000..c792107539 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt @@ -0,0 +1,72 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.migration + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.wire.android.navigation.SavedStateViewModel +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.navArgs +import com.wire.kalium.logic.data.conversation.ConversationDetails +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ConversationMigrationViewModel @Inject constructor( + override val savedStateHandle: SavedStateHandle, + private val observeConversationDetails: ObserveConversationDetailsUseCase +) : SavedStateViewModel(savedStateHandle) { + + /** + * Represents the target conversation, after a conversation migration. + * The target conversation is the active one-on-one conversation ID if the current conversation + * is migrated to a different conversation. + * If this conversation was not migrated to another one, the target conversation is null. + */ + var migratedConversationId by mutableStateOf(null) + private set + + private val conversationNavArgs = savedStateHandle.navArgs() + private val conversationId: QualifiedID = conversationNavArgs.conversationId + + init { + viewModelScope.launch { + observeConversationDetails(conversationId) + .filterIsInstance() + .map { it.conversationDetails } + .filterIsInstance() + .collectLatest { + val activeOneOnOneConversationId = it.otherUser.activeOneOnOneConversationId + val wasThisConversationMigrated = activeOneOnOneConversationId != conversationId + if (wasThisConversationMigrated) { + migratedConversationId = activeOneOnOneConversationId + } + } + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index 0a34ec07df..721a678030 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -403,13 +403,34 @@ sealed class UIMessageContent { } ) - class HistoryLost : SystemMessage(R.drawable.ic_info, R.string.label_system_message_conversation_history_lost, true) class MLSWrongEpochWarning : SystemMessage( iconResId = R.drawable.ic_info, stringResId = R.string.label_system_message_conversation_mls_wrong_epoch_error_handled, isSmallIcon = true ) + data class ConversationProtocolChanged( + val protocol: Conversation.Protocol + ) : SystemMessage( + R.drawable.ic_info, + when (protocol) { + Conversation.Protocol.PROTEUS -> R.string.label_system_message_conversation_protocol_changed_proteus + Conversation.Protocol.MIXED -> R.string.label_system_message_conversation_protocol_changed_mixed + Conversation.Protocol.MLS -> R.string.label_system_message_conversation_protocol_changed_mls + } + ) + + object HistoryLost : SystemMessage( + R.drawable.ic_info, + R.string.label_system_message_conversation_history_lost, + true) + + object HistoryLostProtocolChanged : SystemMessage( + R.drawable.ic_info, + R.string.label_system_message_conversation_history_lost_protocol_changed, + true + ) + data class ConversationMessageCreated( val author: UIText, val isAuthorSelfUser: Boolean = false, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46b94a500f..19f06ff222 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,7 +196,6 @@ Give Feedback Report Bug Debug Settings - API VERSIONING Force API versioning update Update Support @@ -594,6 +593,10 @@ deactivated You haven\'t used this device for a while. some messages may not appear here. The MLS group key was updated without our knowledge. This could happen due to lost messages between backends, or a bug. We have automatically rejoined the conversation, but you may have lost messages. + You didn\'t update your app in time. Some messages may not appear here. + Migration of encryption protocol was canceled. + Migration of encryption protocol has started. Make sure you all your Wire clients are updated. + Migration of encryption protocol is completed. Wire clients which haven\'t been updated will stop receiving messages. on off **You** started the conversation @@ -941,6 +944,8 @@ Enable Logging Proteus Options Enable encrypted proteus storage + Debug Tools + Disable event processing Debug Settings Client ID Device ID: %1$s diff --git a/app/src/test/kotlin/com/wire/android/framework/TestClient.kt b/app/src/test/kotlin/com/wire/android/framework/TestClient.kt index 8b21b864de..de2122fc3d 100644 --- a/app/src/test/kotlin/com/wire/android/framework/TestClient.kt +++ b/app/src/test/kotlin/com/wire/android/framework/TestClient.kt @@ -30,7 +30,16 @@ object TestClient { val CLIENT_ID = ClientId("test") val CLIENT = Client( - CLIENT_ID, ClientType.Permanent, Instant.DISTANT_FUTURE, Instant.DISTANT_PAST, false, - isValid = true, DeviceType.Desktop, "label", null, null + id = CLIENT_ID, + type = ClientType.Permanent, + registrationTime = Instant.DISTANT_FUTURE, + lastActive = Instant.DISTANT_PAST, + isVerified = false, + isValid = true, + deviceType = DeviceType.Desktop, + label = "label", + model = null, + isMLSCapable = false, + mlsPublicKeys = null ) } diff --git a/app/src/test/kotlin/com/wire/android/framework/TestUser.kt b/app/src/test/kotlin/com/wire/android/framework/TestUser.kt index f159b35cc4..00f1bd9f69 100644 --- a/app/src/test/kotlin/com/wire/android/framework/TestUser.kt +++ b/app/src/test/kotlin/com/wire/android/framework/TestUser.kt @@ -26,6 +26,7 @@ import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.SelfUser +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId @@ -46,7 +47,8 @@ object TestUser { connectionStatus = ConnectionState.ACCEPTED, previewPicture = UserAssetId("value", "domain"), completePicture = UserAssetId("value", "domain"), - availabilityStatus = UserAvailabilityStatus.AVAILABLE + availabilityStatus = UserAvailabilityStatus.AVAILABLE, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) val OTHER_USER = OtherUser( USER_ID.copy(value = "otherValue"), @@ -64,7 +66,8 @@ object TestUser { botService = null, deleted = false, defederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) val MEMBER_SELF = MemberDetails(SELF_USER, Member.Role.Admin) val MEMBER_OTHER = MemberDetails(OTHER_USER, Member.Role.Member) diff --git a/app/src/test/kotlin/com/wire/android/mapper/MessageContentMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/MessageContentMapperTest.kt index 8cdd5fcb4e..f9706cfa98 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/MessageContentMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/MessageContentMapperTest.kt @@ -80,7 +80,7 @@ class MessageContentMapperTest { coEvery { regularMessageMapper.mapMessage(any(), any(), any()) } returns UIMessageContent.TextMessage( MessageBody(UIText.DynamicString("some message text")) ) - coEvery { systemMessageContentMapper.mapMessage(any(), any()) } returns UIMessageContent.SystemMessage.HistoryLost() + coEvery { systemMessageContentMapper.mapMessage(any(), any()) } returns UIMessageContent.SystemMessage.HistoryLost } fun arrange() = this to messageContentMapper diff --git a/app/src/test/kotlin/com/wire/android/mapper/RegularMessageContentMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/RegularMessageContentMapperTest.kt index a11de60884..c3dfc9e2ec 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/RegularMessageContentMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/RegularMessageContentMapperTest.kt @@ -39,6 +39,7 @@ import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase @@ -285,7 +286,8 @@ class RegularMessageContentMapperTest { botService = null, deleted = false, defederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) val userMembers = listOf(TestUser.MEMBER_SELF.user, TestUser.MEMBER_OTHER.user) diff --git a/app/src/test/kotlin/com/wire/android/mapper/UIParticipantMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/UIParticipantMapperTest.kt index 451504821c..9dab7f00cd 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/UIParticipantMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/UIParticipantMapperTest.kt @@ -34,6 +34,7 @@ import com.wire.kalium.logic.data.message.UserSummary import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.SelfUser +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType @@ -141,7 +142,8 @@ fun testSelfUser(i: Int): SelfUser = SelfUser( connectionStatus = ConnectionState.NOT_CONNECTED, previewPicture = null, completePicture = null, - availabilityStatus = UserAvailabilityStatus.NONE + availabilityStatus = UserAvailabilityStatus.NONE, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) fun testOtherUser(i: Int): OtherUser = OtherUser( @@ -160,7 +162,8 @@ fun testOtherUser(i: Int): OtherUser = OtherUser( botService = null, deleted = false, defederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) fun testUIParticipant(i: Int): UIParticipant = UIParticipant( diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelTest.kt index b21eca0c72..6dbcdc914f 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelTest.kt @@ -24,12 +24,9 @@ import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.mockUri import com.wire.android.datastore.UserDataStore +import com.wire.android.framework.TestClient import com.wire.android.util.EMPTY import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.data.client.Client -import com.wire.kalium.logic.data.client.ClientType -import com.wire.kalium.logic.data.client.DeviceType -import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.feature.client.GetOrRegisterClientUseCase import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase @@ -42,7 +39,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import kotlinx.datetime.Instant import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeInstanceOf @@ -167,10 +163,6 @@ class RegisterDeviceViewModelTest { } companion object { - val CLIENT_ID = ClientId("test") - val CLIENT = Client( - CLIENT_ID, ClientType.Permanent, Instant.DISTANT_FUTURE, Instant.DISTANT_PAST, false, - isValid = true, DeviceType.Desktop, "label", null, null - ) + val CLIENT = TestClient.CLIENT } } diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt index e930939ae6..cdd4f89381 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt @@ -30,6 +30,7 @@ import com.wire.android.config.mockUri import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.AuthServerConfigProvider import com.wire.android.di.ClientScopeProvider +import com.wire.android.framework.TestClient import com.wire.android.ui.authentication.login.LoginError import com.wire.android.ui.common.textfield.CodeFieldValue import com.wire.android.util.EMPTY @@ -40,10 +41,6 @@ import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.configuration.server.CommonApiVersionType import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.auth.verification.VerifiableAction -import com.wire.kalium.logic.data.client.Client -import com.wire.kalium.logic.data.client.ClientType -import com.wire.kalium.logic.data.client.DeviceType -import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.SsoId @@ -70,7 +67,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import kotlinx.datetime.Instant import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeInstanceOf @@ -491,11 +487,7 @@ class LoginEmailViewModelTest { } companion object { - val CLIENT_ID = ClientId("test") - val CLIENT = Client( - CLIENT_ID, ClientType.Permanent, Instant.DISTANT_FUTURE, Instant.DISTANT_PAST, false, - isValid = true, DeviceType.Desktop, "label", null, null - ) + val CLIENT = TestClient.CLIENT val SSO_ID: SsoId = SsoId("scim_id", null, null) val AUTH_TOKEN = AuthTokens( userId = UserId("user_id", "domain"), diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt index c9d56336b0..5d99c25db8 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt @@ -27,6 +27,7 @@ import com.wire.android.config.mockUri import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.AuthServerConfigProvider import com.wire.android.di.ClientScopeProvider +import com.wire.android.framework.TestClient import com.wire.android.ui.authentication.login.LoginError import com.wire.android.ui.common.dialogs.CustomServerDialogState import com.wire.android.util.EMPTY @@ -38,10 +39,6 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.configuration.server.CommonApiVersionType import com.wire.kalium.logic.configuration.server.ServerConfig -import com.wire.kalium.logic.data.client.Client -import com.wire.kalium.logic.data.client.ClientType -import com.wire.kalium.logic.data.client.DeviceType -import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.SsoId import com.wire.kalium.logic.data.user.UserId @@ -69,7 +66,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import kotlinx.datetime.Instant import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo @@ -80,7 +76,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.io.IOException -import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(CoroutineTestExtension::class) @@ -511,19 +506,7 @@ class LoginSSOViewModelTest { } companion object { - val CLIENT_ID = ClientId("test") - val CLIENT = Client( - CLIENT_ID, - ClientType.Permanent, - Instant.DISTANT_FUTURE, - Instant.DISTANT_PAST, - false, - isValid = true, - DeviceType.Desktop, - "label", - null, - null - ) + val CLIENT = TestClient.CLIENT val SSO_ID: SsoId = SsoId("scim_id", null, null) val AUTH_TOKEN = AuthTokens( userId = UserId("user_id", "domain"), diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/AuthorHeaderHelperTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/AuthorHeaderHelperTest.kt index 72960048c8..06c592dcca 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/AuthorHeaderHelperTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/AuthorHeaderHelperTest.kt @@ -187,7 +187,7 @@ class AuthorHeaderHelperTest { userId = userId ), source = MessageSource.OtherUser, - messageContent = UIMessageContent.SystemMessage.HistoryLost() + messageContent = UIMessageContent.SystemMessage.HistoryLost ) private fun testPingMessage( diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt new file mode 100644 index 0000000000..629168d25d --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt @@ -0,0 +1,113 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.migration + +import androidx.lifecycle.SavedStateHandle +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.ConversationNavArgs +import com.wire.android.ui.navArgs +import com.wire.kalium.logic.data.conversation.ConversationDetails +import com.wire.kalium.logic.data.conversation.LegalHoldStatus +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.type.UserType +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +@ExtendWith(NavigationTestExtension::class) +class ConversationMigrationViewModelTest { + + @Test + fun givenActiveOneOnOneMatchesCurrentConversation_thenMigratedConversationShouldBeNull() = runTest { + val (_, conversationMigrationViewModel) = arrange { + withConversationDetailsReturning( + ConversationDetails.OneOne( + conversation = TestConversation.ONE_ON_ONE, + otherUser = TestUser.OTHER_USER.copy(activeOneOnOneConversationId = conversationId), + legalHoldStatus = LegalHoldStatus.ENABLED, + userType = UserType.NONE, + unreadEventCount = mapOf(), + lastMessage = null + ) + ) + } + + conversationMigrationViewModel.migratedConversationId shouldBe null + } + + @Test + fun givenActiveOneOnOneDiffersFromCurrentConversation_thenMigratedConversationShouldBeTheOneInDetails() = runTest { + val expectedActiveOneOnOneId = ConversationId("expectedActiveOneOnOneId", "testDomain") + val (_, conversationMigrationViewModel) = arrange { + withConversationDetailsReturning( + ConversationDetails.OneOne( + conversation = TestConversation.ONE_ON_ONE, + otherUser = TestUser.OTHER_USER.copy(activeOneOnOneConversationId = expectedActiveOneOnOneId), + legalHoldStatus = LegalHoldStatus.ENABLED, + userType = UserType.NONE, + unreadEventCount = mapOf(), + lastMessage = null + ) + ) + } + + conversationMigrationViewModel.migratedConversationId shouldBeEqualTo expectedActiveOneOnOneId + } + + private class Arrangement(private val configure: Arrangement.() -> Unit) { + + @MockK + lateinit var observeConversationDetailsUseCase: ObserveConversationDetailsUseCase + + @MockK + lateinit var savedStateHandle: SavedStateHandle + + init { + MockKAnnotations.init(this) + every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId) + } + + fun withConversationDetailsReturning(conversationDetails: ConversationDetails) = apply { + coEvery { observeConversationDetailsUseCase(conversationId) } returns + flowOf(ObserveConversationDetailsUseCase.Result.Success(conversationDetails)) + } + + fun arrange(): Pair = run { + configure() + this@Arrangement to ConversationMigrationViewModel(savedStateHandle, observeConversationDetailsUseCase) + } + } + + private companion object { + val conversationId = TestConversation.ID + + fun arrange(configure: Arrangement.() -> Unit) = Arrangement(configure).arrange() + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt index d9edc63d8f..50eb8b96de 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt @@ -41,6 +41,7 @@ import com.wire.kalium.logic.data.conversation.MutedConversationStatus.AllAllowe import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase @@ -312,6 +313,7 @@ class MediaGalleryViewModelTest { 1, null, ConnectionState.ACCEPTED, null, null, UserType.INTERNAL, UserAvailabilityStatus.AVAILABLE, + setOf(SupportedProtocol.PROTEUS), null, false, defederated = false, 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 35f3130e16..3976b38b2a 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 @@ -40,6 +40,7 @@ import com.wire.kalium.logic.data.publicuser.model.UserSearchResult import com.wire.kalium.logic.data.service.ServiceDetails import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.type.UserType @@ -177,7 +178,8 @@ internal class NewConversationViewModelArrangement { botService = null, deleted = false, defederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) val FEDERATED_KNOWN_USER = OtherUser( @@ -196,7 +198,8 @@ internal class NewConversationViewModelArrangement { botService = null, deleted = false, defederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt index a45a035c8a..a7c48a3c74 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt @@ -35,6 +35,7 @@ import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.team.Team import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType @@ -223,7 +224,8 @@ class OtherUserProfileScreenViewModelTest { botService = null, deleted = false, defederated = false, - isProteusVerified = false + isProteusVerified = false, + supportedProtocols = setOf(SupportedProtocol.PROTEUS) ) val TEAM = Team("some_id", "name", "icon") val CONVERSATION = Conversation( diff --git a/kalium b/kalium index 675404c321..4d801fc83f 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 675404c32125485fc04817618e9f9f7a1efe3374 +Subproject commit 4d801fc83ff23b824c987d4347e2d8083bd850c6