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 1e881239543..bea34f22067 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -440,4 +440,19 @@ class UseCaseModule { fun provideObserveIsAppLockEditableUseCase( @KaliumCoreLogic coreLogic: CoreLogic ): ObserveIsAppLockEditableUseCase = coreLogic.getGlobalScope().observeIsAppLockEditableUseCase + + @ViewModelScoped + @Provides + fun provideObserveLegalHoldRequestUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = + coreLogic.getSessionScope(currentAccount).observeLegalHoldRequest + + @ViewModelScoped + @Provides + fun provideObserveLegalHoldForSelfUserUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = + coreLogic.getSessionScope(currentAccount).observeLegalHoldForSelfUser + + @ViewModelScoped + @Provides + fun provideObserveLegalHoldForUserUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = + coreLogic.getSessionScope(currentAccount).observeLegalHoldStateForUser } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 21f7cb8bad9..7245801c5fb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -159,6 +159,9 @@ class WireActivity : AppCompatActivity() { appLogger.i("$TAG persistent connection status") viewModel.observePersistentConnectionStatus() + appLogger.i("$TAG legal hold requested status") + legalHoldRequestedViewModel.observeLegalHoldRequest() + appLogger.i("$TAG start destination") val startDestination = when (viewModel.initialAppState) { InitialAppState.NOT_MIGRATED -> MigrationScreenDestination diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldBaseBanner.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldBaseBanner.kt new file mode 100644 index 00000000000..76d966bcc81 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldBaseBanner.kt @@ -0,0 +1,63 @@ +/* + * Wire + * Copyright (C) 2024 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.legalhold.banner + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.wire.android.ui.common.LegalHoldIndicator +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions + +@Composable +fun LegalHoldBaseBanner( + onClick: () -> Unit = {}, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + modifier = modifier + .clip(RoundedCornerShape(dimensions().spacing12x)) + .background(colorsScheme().surface) + .border( + width = dimensions().spacing1x, + shape = RoundedCornerShape(dimensions().spacing12x), + color = colorsScheme().error + ) + .clickable(onClick = onClick) + .heightIn(min = dimensions().legalHoldBannerMinHeight) + .padding( + horizontal = dimensions().spacing12x, + vertical = dimensions().spacing4x + ) + ) { + LegalHoldIndicator() + content() + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldPendingBanner.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldPendingBanner.kt new file mode 100644 index 00000000000..50e51b1f609 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldPendingBanner.kt @@ -0,0 +1,63 @@ +/* + * Wire + * Copyright (C) 2024 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.legalhold.banner + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import com.wire.android.R +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.typography +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +fun LegalHoldPendingBanner( + onClick: () -> Unit = {}, + modifier: Modifier = Modifier, +) { + LegalHoldBaseBanner(onClick = onClick, modifier = modifier) { + Row { + Text( + text = stringResource(id = R.string.legal_hold_is_pending_label), + style = typography().label01, + color = colorsScheme().onSurface, + ) + Text( + text = stringResource(id = R.string.legal_hold_accept), + style = typography().label02, + textDecoration = TextDecoration.Underline, + color = colorsScheme().onSurface, + modifier = Modifier.padding(start = dimensions().spacing2x), + ) + } + } +} + +@Composable +@PreviewMultipleThemes +fun PreviewLegalHoldPendingBanner() { + WireTheme { + LegalHoldPendingBanner() + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldSubjectBanner.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldSubjectBanner.kt index 959c402b554..afb65eb15aa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldSubjectBanner.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/banner/LegalHoldSubjectBanner.kt @@ -17,26 +17,13 @@ */ package com.wire.android.ui.legalhold.banner -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape 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.platform.LocalContext import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.dp import com.wire.android.R -import com.wire.android.ui.common.LegalHoldIndicator import com.wire.android.ui.common.colorsScheme -import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.typography import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes @@ -47,25 +34,7 @@ fun LegalHoldSubjectBanner( onClick: () -> Unit = {}, modifier: Modifier = Modifier, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), - modifier = modifier - .clip(RoundedCornerShape(dimensions().spacing12x)) - .background(colorsScheme().surface) - .border( - width = dimensions().spacing1x, - shape = RoundedCornerShape(dimensions().spacing12x), - color = colorsScheme().error - ) - .clickable(onClick = onClick) - .heightIn(min = 26.dp) - .padding( - horizontal = dimensions().spacing12x, - vertical = dimensions().spacing4x - ) - ) { - LegalHoldIndicator() + LegalHoldBaseBanner(onClick = onClick, modifier = modifier) { val resources = LocalContext.current.resources Text( text = resources.stringWithStyledArgs( diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt index 27e09d8ff6a..b306bc7ed66 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt @@ -91,7 +91,7 @@ class LegalHoldRequestedViewModel @Inject constructor( } } - init { + fun observeLegalHoldRequest() { viewModelScope.launch { legalHoldRequestDataStateFlow.collectLatest { legalHoldRequestData -> state = when (legalHoldRequestData) { diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectBaseDialog.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectBaseDialog.kt index 5f25ced75e4..78359354b3b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectBaseDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectBaseDialog.kt @@ -33,7 +33,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun LegalHoldSubjectBaseDialog( - name: String, + title: String, customInfo: String? = null, withDefaultInfo: Boolean, cancelText: String, @@ -45,7 +45,7 @@ fun LegalHoldSubjectBaseDialog( if (withDefaultInfo) stringResource(id = R.string.legal_hold_subject_dialog_description) else null ).joinToString("\n\n") WireDialog( - title = stringResource(id = R.string.legal_hold_subject_dialog_title, name), + title = title, text = text, onDismiss = dialogDismissed, buttonsHorizontalAlignment = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectConnectionDialog.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectConnectionDialog.kt index 937c2ee430c..e2fcf91eb4d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectConnectionDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectConnectionDialog.kt @@ -30,7 +30,7 @@ fun LegalHoldSubjectConnectionDialog( connectClicked: () -> Unit, ) { LegalHoldSubjectBaseDialog( - name = userName, + title = stringResource(id = R.string.legal_hold_subject_dialog_title, userName), withDefaultInfo = true, cancelText = stringResource(id = R.string.label_cancel), dialogDismissed = dialogDismissed, diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectConversationDialog.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectConversationDialog.kt index 57d5b9e3ba5..0ff5998d140 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectConversationDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectConversationDialog.kt @@ -29,7 +29,7 @@ fun LegalHoldSubjectConversationDialog( dialogDismissed: () -> Unit, ) { LegalHoldSubjectBaseDialog( - name = conversationName, + title = stringResource(id = R.string.legal_hold_subject_dialog_title, conversationName), customInfo = stringResource(id = R.string.legal_hold_subject_dialog_description_group), withDefaultInfo = true, cancelText = stringResource(id = R.string.label_close), diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectMessageDialog.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectMessageDialog.kt index a57b7935190..b670d654c3c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectMessageDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectMessageDialog.kt @@ -30,7 +30,7 @@ fun LegalHoldSubjectMessageDialog( sendAnywayClicked: () -> Unit, ) { LegalHoldSubjectBaseDialog( - name = conversationName, + title = stringResource(id = R.string.legal_hold_subject_dialog_title, conversationName), customInfo = stringResource(id = R.string.legal_hold_subject_dialog_description_message), withDefaultInfo = false, cancelText = stringResource(id = R.string.label_cancel), diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectProfileDialog.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectProfileDialog.kt index 63de0ea519a..e2e8f707421 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectProfileDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/subject/LegalHoldSubjectProfileDialog.kt @@ -29,7 +29,15 @@ fun LegalHoldSubjectProfileDialog( dialogDismissed: () -> Unit, ) { LegalHoldSubjectBaseDialog( - name = userName, + title = stringResource(id = R.string.legal_hold_subject_dialog_title, userName), + withDefaultInfo = true, + cancelText = stringResource(id = R.string.label_close), + dialogDismissed = dialogDismissed) +} +@Composable +fun LegalHoldSubjectProfileSelfDialog(dialogDismissed: () -> Unit) { + LegalHoldSubjectBaseDialog( + title = stringResource(id = R.string.legal_hold_subject_self_dialog_title), withDefaultInfo = true, cancelText = stringResource(id = R.string.label_close), dialogDismissed = dialogDismissed) @@ -42,3 +50,10 @@ fun PreviewLegalHoldSubjectProfileDialog() { LegalHoldSubjectProfileDialog("username", {}) } } +@Composable +@PreviewMultipleThemes +fun PreviewLegalHoldSubjectProfileSelfDialog() { + WireTheme { + LegalHoldSubjectProfileSelfDialog {} + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt index 75d3bc4002d..e777df7edb9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt @@ -192,7 +192,9 @@ data class WireDimensions( // Conversation options val conversationOptionsItemMinHeight: Dp, // Import media - val importedMediaAssetSize: Dp + val importedMediaAssetSize: Dp, + // legal hold banner + val legalHoldBannerMinHeight: Dp, ) private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( @@ -334,7 +336,8 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( ongoingCallLabelHeight = 28.dp, audioMessageHeight = 48.dp, importedMediaAssetSize = 120.dp, - typingIndicatorHeight = 24.dp + typingIndicatorHeight = 24.dp, + legalHoldBannerMinHeight = 26.dp, ) private val DefaultPhoneLandscapeWireDimensions: WireDimensions = DefaultPhonePortraitWireDimensions diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt index f15cb501bb2..d96abf4afcd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt @@ -61,8 +61,6 @@ import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.banner.SecurityClassificationBannerForUser import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator -import com.wire.android.ui.common.spacers.VerticalSpace -import com.wire.android.ui.home.conversations.details.SearchAndMediaRow import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography @@ -91,9 +89,6 @@ fun UserProfileInfo( delayToShowPlaceholderIfNoAsset: Duration = 200.milliseconds, isProteusVerified: Boolean = false, isMLSVerified: Boolean = false, - onSearchConversationMessagesClick: () -> Unit = {}, - onConversationMediaClick: () -> Unit = {}, - shouldShowSearchButton: Boolean = false ) { Column( horizontalAlignment = CenterHorizontally, @@ -233,14 +228,6 @@ fun UserProfileInfo( modifier = Modifier.padding(top = dimensions().spacing8x) ) } - - if (shouldShowSearchButton) { - VerticalSpace.x24() - SearchAndMediaRow( - onSearchConversationMessagesClick = onSearchConversationMessagesClick, - onConversationMediaClick = onConversationMediaClick - ) - } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index e7dd1482b01..a05f9898bf0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -48,10 +48,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -67,6 +67,7 @@ import com.wire.android.ui.authentication.devices.model.Device import com.wire.android.ui.common.CollapsingTopBarScaffold import com.wire.android.ui.common.MoreOptionIcon import com.wire.android.ui.common.TabItem +import com.wire.android.ui.common.VisibilityState import com.wire.android.ui.common.WireTabRow import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout import com.wire.android.ui.common.bottomsheet.WireModalSheetState @@ -80,6 +81,7 @@ import com.wire.android.ui.common.dialogs.UnblockUserDialogContent import com.wire.android.ui.common.dialogs.UnblockUserDialogState import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.snackbar.LocalSnackbarHostState +import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.common.topBarElevation import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar @@ -89,9 +91,12 @@ import com.wire.android.ui.destinations.ConversationMediaScreenDestination import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.DeviceDetailsScreenDestination import com.wire.android.ui.destinations.SearchConversationMessagesScreenDestination +import com.wire.android.ui.home.conversations.details.SearchAndMediaRow import com.wire.android.ui.home.conversations.details.dialog.ClearConversationContentDialog import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.Membership +import com.wire.android.ui.legalhold.banner.LegalHoldSubjectBanner +import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectProfileDialog import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions @@ -100,6 +105,7 @@ import com.wire.android.ui.userprofile.common.UserProfileInfo import com.wire.android.ui.userprofile.group.RemoveConversationMemberState import com.wire.android.ui.userprofile.other.bottomsheet.OtherUserBottomSheetState import com.wire.android.ui.userprofile.other.bottomsheet.OtherUserProfileBottomSheetContent +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.ConnectionState import kotlinx.coroutines.CoroutineScope @@ -154,6 +160,8 @@ fun OtherUserProfileScreen( } } + val legalHoldSubjectDialogState = rememberVisibilityState() + OtherProfileScreenContent( scope = scope, state = viewModel.state, @@ -172,7 +180,8 @@ fun OtherUserProfileScreen( onSearchConversationMessagesClick = onSearchConversationMessagesClick, navigateBack = navigator::navigateBack, navigationIconType = NavigationIconType.Close, - onConversationMediaClick = onConversationMediaClick + onConversationMediaClick = onConversationMediaClick, + onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } }, ) LaunchedEffect(Unit) { @@ -185,6 +194,10 @@ fun OtherUserProfileScreen( sheetState.hide() } } + + VisibilityState(legalHoldSubjectDialogState) { + LegalHoldSubjectProfileDialog(viewModel.state.userName, legalHoldSubjectDialogState::dismiss) + } } @OptIn(ExperimentalFoundationApi::class) @@ -205,7 +218,8 @@ fun OtherProfileScreenContent( onOpenDeviceDetails: (Device) -> Unit = {}, onSearchConversationMessagesClick: () -> Unit, onConversationMediaClick: () -> Unit = {}, - navigateBack: () -> Unit = {} + navigateBack: () -> Unit = {}, + onLegalHoldLearnMoreClick: () -> Unit = {}, ) { val otherUserProfileScreenState = rememberOtherUserProfileScreenState() val blockUserDialogState = rememberVisibilityState() @@ -283,7 +297,8 @@ fun OtherProfileScreenContent( TopBarCollapsing( state = state, onSearchConversationMessagesClick = onSearchConversationMessagesClick, - onConversationMediaClick = onConversationMediaClick + onConversationMediaClick = onConversationMediaClick, + onLegalHoldLearnMoreClick = onLegalHoldLearnMoreClick, ) }, topBarFooter = { TopBarFooter(state, pagerState, tabBarElevationState, tabItems, currentTabState, scope) }, @@ -384,29 +399,44 @@ private fun TopBarHeader( private fun TopBarCollapsing( state: OtherUserProfileState, onSearchConversationMessagesClick: () -> Unit, - onConversationMediaClick: () -> Unit = {} + onConversationMediaClick: () -> Unit = {}, + onLegalHoldLearnMoreClick: () -> Unit = {}, ) { Crossfade( targetState = state, label = "OtherUserProfileScreenTopBarCollapsing" ) { targetState -> - UserProfileInfo( - userId = targetState.userId, - isLoading = targetState.isAvatarLoading, - avatarAsset = targetState.userAvatarAsset, - fullName = targetState.fullName, - userName = targetState.userName, - teamName = targetState.teamName, - membership = targetState.membership, - editableState = EditableState.NotEditable, + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(bottom = dimensions().spacing16x), - connection = targetState.connectionState, - isProteusVerified = targetState.isProteusVerified, - isMLSVerified = targetState.isMLSVerified, - onSearchConversationMessagesClick = onSearchConversationMessagesClick, - shouldShowSearchButton = state.shouldShowSearchButton(), - onConversationMediaClick = onConversationMediaClick - ) + ) { + UserProfileInfo( + userId = targetState.userId, + isLoading = targetState.isAvatarLoading, + avatarAsset = targetState.userAvatarAsset, + fullName = targetState.fullName, + userName = targetState.userName, + teamName = targetState.teamName, + membership = targetState.membership, + editableState = EditableState.NotEditable, + connection = targetState.connectionState, + isProteusVerified = targetState.isProteusVerified, + isMLSVerified = targetState.isMLSVerified, + ) + if (state.isUnderLegalHold) { + LegalHoldSubjectBanner( + onClick = onLegalHoldLearnMoreClick, + modifier = Modifier.padding(top = dimensions().spacing8x) + ) + } + if (state.shouldShowSearchButton()) { + VerticalSpace.x24() + SearchAndMediaRow( + onSearchConversationMessagesClick = onSearchConversationMessagesClick, + onConversationMediaClick = onConversationMediaClick + ) + } + } } } @@ -549,12 +579,39 @@ enum class OtherUserProfileTabItem(@StringRes override val titleResId: Int) : Ta @OptIn(ExperimentalMaterial3Api::class) @Composable -@Preview(name = "Connected") +@PreviewMultipleThemes +fun PreviewOtherProfileScreenGroupMemberContent() { + WireTheme { + OtherProfileScreenContent( + scope = rememberCoroutineScope(), + state = OtherUserProfileState.PREVIEW.copy( + connectionState = ConnectionState.ACCEPTED, + isUnderLegalHold = true, + ), + navigationIconType = NavigationIconType.Back, + requestInProgress = false, + sheetState = rememberWireModalSheetState(), + openBottomSheet = {}, + closeBottomSheet = {}, + eventsHandler = OtherUserProfileEventsHandler.PREVIEW, + bottomSheetEventsHandler = OtherUserProfileBottomSheetEventsHandler.PREVIEW, + onSearchConversationMessagesClick = {} + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@PreviewMultipleThemes fun PreviewOtherProfileScreenContent() { WireTheme { OtherProfileScreenContent( scope = rememberCoroutineScope(), - state = OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.ACCEPTED), + state = OtherUserProfileState.PREVIEW.copy( + connectionState = ConnectionState.ACCEPTED, + isUnderLegalHold = true, + groupState = null + ), navigationIconType = NavigationIconType.Back, requestInProgress = false, sheetState = rememberWireModalSheetState(), @@ -569,12 +626,15 @@ fun PreviewOtherProfileScreenContent() { @OptIn(ExperimentalMaterial3Api::class) @Composable -@Preview(name = "Not Connected") +@PreviewMultipleThemes fun PreviewOtherProfileScreenContentNotConnected() { WireTheme { OtherProfileScreenContent( scope = rememberCoroutineScope(), - state = OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.CANCELLED), + state = OtherUserProfileState.PREVIEW.copy( + connectionState = ConnectionState.CANCELLED, + isUnderLegalHold = true, + ), navigationIconType = NavigationIconType.Back, requestInProgress = false, sheetState = rememberWireModalSheetState(), diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index 654aa596a3b..9795b4b259d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -72,12 +72,15 @@ import com.wire.kalium.logic.feature.e2ei.CertificateStatus import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificateStatusResult import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificateStatusUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase +import com.wire.kalium.logic.feature.legalhold.LegalHoldState +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForUserUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn @@ -96,6 +99,7 @@ class OtherUserProfileScreenViewModel @Inject constructor( private val unblockUser: UnblockUserUseCase, private val observeOneToOneConversation: GetOneToOneConversationUseCase, private val observeUserInfo: ObserveUserInfoUseCase, + private val observeLegalHoldStateForUser: ObserveLegalHoldStateForUserUseCase, private val userTypeMapper: UserTypeMapper, private val wireSessionImageLoader: WireSessionImageLoader, private val observeConversationRoleForUser: ObserveConversationRoleForUserUseCase, @@ -134,6 +138,14 @@ class OtherUserProfileScreenViewModel @Inject constructor( observeUserInfoAndUpdateViewState() persistClients() getMLSVerificationStatus() + observeLegalHoldStatus() + } + + private fun observeLegalHoldStatus() { + viewModelScope.launch { + observeLegalHoldStateForUser(userId) + .collectLatest { state = state.copy(isUnderLegalHold = it is LegalHoldState.Enabled) } + } } private fun getMLSVerificationStatus() { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt index a8b751828d3..022638ee4fe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt @@ -49,7 +49,8 @@ data class OtherUserProfileState( val otherUserDevices: List = listOf(), val blockingState: BlockingState = BlockingState.CAN_NOT_BE_BLOCKED, val isProteusVerified: Boolean = false, - val isMLSVerified: Boolean = false + val isMLSVerified: Boolean = false, + val isUnderLegalHold: Boolean = false, ) { fun updateMuteStatus(status: MutedConversationStatus): OtherUserProfileState { return conversationSheetContent?.let { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index a7b3c290e0b..cf508c968e4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -45,7 +45,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.hilt.navigation.compose.hiltViewModel @@ -65,6 +64,7 @@ import com.wire.android.ui.common.ArrowRightIcon import com.wire.android.ui.common.RowItemTemplate import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.UserStatusIndicator +import com.wire.android.ui.common.VisibilityState import com.wire.android.ui.common.WireDropDown import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton @@ -72,6 +72,7 @@ import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dialogs.ProgressDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState @@ -81,8 +82,14 @@ import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.home.conversations.search.HighlightName import com.wire.android.ui.home.conversations.search.HighlightSubtitle import com.wire.android.ui.home.conversationslist.common.FolderHeader +import com.wire.android.ui.legalhold.banner.LegalHoldPendingBanner +import com.wire.android.ui.legalhold.banner.LegalHoldSubjectBanner +import com.wire.android.ui.legalhold.banner.LegalHoldUIState +import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedDialog +import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedState +import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedViewModel +import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectProfileSelfDialog import com.wire.android.ui.theme.WireTheme -import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.userprofile.common.EditableState import com.wire.android.ui.userprofile.common.UserProfileInfo @@ -104,8 +111,11 @@ import com.wire.kalium.logic.data.user.UserId fun SelfUserProfileScreen( navigator: Navigator, viewModelSelf: SelfUserProfileViewModel = hiltViewModel(), + legalHoldRequestedViewModel: LegalHoldRequestedViewModel = hiltViewModel(), avatarPickerResultRecipient: ResultRecipient ) { + val legalHoldSubjectDialogState = rememberVisibilityState() + SelfUserProfileContent( state = viewModelSelf.userProfileState, onCloseClick = navigator::navigateBack, @@ -118,7 +128,8 @@ fun SelfUserProfileScreen( onStatusChange = viewModelSelf::changeStatus, onNotShowRationaleAgainChange = viewModelSelf::dialogCheckBoxStateChanged, onMessageShown = viewModelSelf::clearErrorMessage, - onMaxAccountReachedDialogDismissed = viewModelSelf::onMaxAccountReachedDialogDismissed, + onLegalHoldAcceptClick = legalHoldRequestedViewModel::show, + onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } }, onOtherAccountClick = { viewModelSelf.switchAccount(it, NavigationSwitchAccountActions(navigator::navigate)) }, isUserInCall = viewModelSelf::isUserInCall ) @@ -138,6 +149,18 @@ fun SelfUserProfileScreen( } } } + + if (legalHoldRequestedViewModel.state is LegalHoldRequestedState.Visible) { + LegalHoldRequestedDialog( + state = legalHoldRequestedViewModel.state as LegalHoldRequestedState.Visible, + passwordChanged = legalHoldRequestedViewModel::passwordChanged, + notNowClicked = legalHoldRequestedViewModel::notNowClicked, + acceptClicked = legalHoldRequestedViewModel::acceptClicked, + ) + } + VisibilityState(legalHoldSubjectDialogState) { + LegalHoldSubjectProfileSelfDialog(legalHoldSubjectDialogState::dismiss) + } } @OptIn(ExperimentalFoundationApi::class) @@ -154,7 +177,8 @@ private fun SelfUserProfileContent( onStatusChange: (UserAvailabilityStatus) -> Unit = {}, onNotShowRationaleAgainChange: (Boolean) -> Unit = {}, onMessageShown: () -> Unit = {}, - onMaxAccountReachedDialogDismissed: () -> Unit = {}, + onLegalHoldAcceptClick: () -> Unit = {}, + onLegalHoldLearnMoreClick: () -> Unit = {}, onOtherAccountClick: (UserId) -> Unit = {}, isUserInCall: () -> Boolean ) { @@ -209,6 +233,20 @@ private fun SelfUserProfileContent( else EditableState.IsEditable(onEditClick) ) } + if (state.legalHoldStatus != LegalHoldUIState.None) { + stickyHeader { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().padding(top = dimensions().spacing8x) + ) { + when (state.legalHoldStatus) { + LegalHoldUIState.Active -> LegalHoldSubjectBanner(onLegalHoldLearnMoreClick) + LegalHoldUIState.Pending -> LegalHoldPendingBanner(onLegalHoldAcceptClick) + LegalHoldUIState.None -> { /* no banner */ } + } + } + } + } if (!state.teamName.isNullOrBlank()) { stickyHeader { CurrentSelfUserStatus( @@ -416,32 +454,35 @@ private fun LoggingOutDialog(isLoggingOut: Boolean) { } } -@Preview(widthDp = 400, heightDp = 800) -@Preview(widthDp = 800) +@PreviewMultipleThemes @Composable fun PreviewSelfUserProfileScreen() { - SelfUserProfileContent( - SelfUserProfileState( - userId = UserId("value", "domain"), - status = UserAvailabilityStatus.BUSY, - fullName = "Tester Tost_long_long_long long long long long long long ", - userName = "userName_long_long_long_long_long_long_long_long_long_long", - teamName = "Best team ever long long long long long long long long long ", - otherAccounts = listOf( - OtherAccount(id = UserId("id1", "domain"), fullName = "Other Name", teamName = "team A"), - OtherAccount(id = UserId("id2", "domain"), fullName = "New Name") + WireTheme { + SelfUserProfileContent( + SelfUserProfileState( + userId = UserId("value", "domain"), + status = UserAvailabilityStatus.BUSY, + fullName = "Tester Tost_long_long_long long long long long long long ", + userName = "userName_long_long_long_long_long_long_long_long_long_long", + teamName = "Best team ever long long long long long long long long long ", + otherAccounts = listOf( + OtherAccount(id = UserId("id1", "domain"), fullName = "Other Name", teamName = "team A"), + OtherAccount(id = UserId("id2", "domain"), fullName = "New Name") + ), + statusDialogData = null, + legalHoldStatus = LegalHoldUIState.Active, ), - statusDialogData = null - ), - isUserInCall = { false } - ) + isUserInCall = { false } + ) + } } -@Preview(widthDp = 800) -@Preview(widthDp = 400) +@PreviewMultipleThemes @Composable fun PreviewCurrentSelfUserStatus() { - CurrentSelfUserStatus(UserAvailabilityStatus.AVAILABLE, onStatusClicked = {}) + WireTheme { + CurrentSelfUserStatus(UserAvailabilityStatus.AVAILABLE, onStatusClicked = {}) + } } @Composable diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt index d6126d92361..58b9b51dd37 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileState.kt @@ -19,6 +19,7 @@ package com.wire.android.ui.userprofile.self import com.wire.android.model.ImageAsset.UserAvatarAsset +import com.wire.android.ui.legalhold.banner.LegalHoldUIState import com.wire.android.ui.userprofile.self.SelfUserProfileViewModel.ErrorCodes import com.wire.android.ui.userprofile.self.dialog.StatusDialogData import com.wire.android.ui.userprofile.self.model.OtherAccount @@ -38,5 +39,6 @@ data class SelfUserProfileState constructor( val isAvatarLoading: Boolean = false, val maxAccountsReached: Boolean = false, // todo. cleanup unused code val isReadOnlyAccount: Boolean = true, - val isLoggingOut: Boolean = false + val isLoggingOut: Boolean = false, + val legalHoldStatus: LegalHoldUIState = LegalHoldUIState.None, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 049607eaa6f..98dd577a9b5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -37,6 +37,7 @@ import com.wire.android.mapper.OtherAccountMapper import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager +import com.wire.android.ui.legalhold.banner.LegalHoldUIState import com.wire.android.ui.userprofile.self.dialog.StatusDialogData import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.WireSessionImageLoader @@ -53,6 +54,9 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.auth.LogoutUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.legalhold.LegalHoldState +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldForSelfUserUseCase +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase import com.wire.kalium.logic.feature.team.GetUpdatedSelfTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase @@ -63,6 +67,7 @@ import com.wire.kalium.logic.functional.getOrNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first @@ -85,6 +90,8 @@ class SelfUserProfileViewModel @Inject constructor( private val observeValidAccounts: ObserveValidAccountsUseCase, private val updateStatus: UpdateSelfAvailabilityStatusUseCase, private val logout: LogoutUseCase, + private val observeLegalHoldRequest: ObserveLegalHoldRequestUseCase, + private val observeLegalHoldForSelfUser: ObserveLegalHoldForSelfUserUseCase, private val dispatchers: DispatcherProvider, private val wireSessionImageLoader: WireSessionImageLoader, private val authServerConfigProvider: AuthServerConfigProvider, @@ -110,6 +117,7 @@ class SelfUserProfileViewModel @Inject constructor( fetchSelfUser() observeEstablishedCall() fetchIsReadOnlyAccount() + observeLegalHoldStatus() } } @@ -168,6 +176,23 @@ class SelfUserProfileViewModel @Inject constructor( } } + private fun observeLegalHoldStatus() { + viewModelScope.launch { + combine( + observeLegalHoldRequest(), + observeLegalHoldForSelfUser() + ) { legalHoldRequestStatus: ObserveLegalHoldRequestUseCase.Result, legalHoldStatus: LegalHoldState -> + when { + legalHoldRequestStatus is ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable -> LegalHoldUIState.Pending + legalHoldStatus is LegalHoldState.Enabled -> LegalHoldUIState.Active + else -> LegalHoldUIState.None + } + } + .distinctUntilChanged() + .collectLatest { userProfileState = userProfileState.copy(legalHoldStatus = it) } + } + } + private fun showErrorMessage() { userProfileState = userProfileState.copy(errorMessageCode = ErrorCodes.DownloadUserInfoError) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98ca13e53a6..4198d745f5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1337,6 +1337,7 @@ Legal hold deactivated Future messages will not be recorded. %1$s is subject to legal hold + You are subject to legal hold All messages, pictures, and documents will be preserved for future access. It includes deleted, edited, and self-deleting messages. At least one person in this conversation is subject to legal hold. Do you still want to send your message? @@ -1347,6 +1348,7 @@ legal hold Legal hold is active Legal hold is pending + Accept You are now subject to legal hold. Legal hold activated for %1$s. You are no longer subject to legal hold. diff --git a/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt index 2da904421b9..3c6cc6fb916 100644 --- a/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt @@ -272,7 +272,7 @@ class LegalHoldRequestedViewModelTest { coEvery { coreLogic.getSessionScope(any()).approveLegalHoldRequest(any()) } returns result } - fun arrange() = this to viewModel + fun arrange() = this to viewModel.apply { observeLegalHoldRequest() } companion object { val UNKNOWN_ERROR = CoreFailure.Unknown(RuntimeException("error")) 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 78473500128..9250c3228ec 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 @@ -40,6 +40,7 @@ import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.feature.connection.BlockUserResult import com.wire.kalium.logic.feature.conversation.GetOneToOneConversationUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleResult +import com.wire.kalium.logic.feature.legalhold.LegalHoldState import com.wire.kalium.logic.feature.user.GetUserInfoResult import io.mockk.Called import io.mockk.coVerify @@ -203,6 +204,28 @@ class OtherUserProfileScreenViewModelTest { assertEquals(null, viewModel.state.conversationSheetContent) } + @Test + fun `given legal hold enabled, then isUnderLegalHold is true`() = runTest { + // given + val (_, viewModel) = OtherUserProfileViewModelArrangement() + .withUserInfo(GetUserInfoResult.Success(OTHER_USER.copy(connectionStatus = ConnectionState.NOT_CONNECTED), TEAM)) + .withLegalHoldState(LegalHoldState.Enabled) + .arrange() + // then + assertEquals(true, viewModel.state.isUnderLegalHold) + } + + @Test + fun `given legal hold disabled, then isUnderLegalHold is false`() = runTest { + // given + val (_, viewModel) = OtherUserProfileViewModelArrangement() + .withUserInfo(GetUserInfoResult.Success(OTHER_USER.copy(connectionStatus = ConnectionState.NOT_CONNECTED), TEAM)) + .withLegalHoldState(LegalHoldState.Disabled) + .arrange() + // then + assertEquals(false, viewModel.state.isUnderLegalHold) + } + companion object { val USER_ID = UserId("some_value", "some_domain") val CONVERSATION_ID = ConversationId("some_value", "some_domain") diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt index 1729d4aa114..cf090fb8556 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt @@ -47,6 +47,8 @@ import com.wire.kalium.logic.feature.e2ei.CertificateStatus import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificateStatusResult import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificateStatusUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase +import com.wire.kalium.logic.feature.legalhold.LegalHoldState +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForUserUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase @@ -67,6 +69,9 @@ internal class OtherUserProfileViewModelArrangement { @MockK lateinit var observeUserInfo: ObserveUserInfoUseCase + @MockK + lateinit var observeLegalHoldStateForUser: ObserveLegalHoldStateForUserUseCase + @MockK lateinit var wireSessionImageLoader: WireSessionImageLoader @@ -120,6 +125,7 @@ internal class OtherUserProfileViewModelArrangement { unblockUser, getOneToOneConversation, observeUserInfo, + observeLegalHoldStateForUser, userTypeMapper, wireSessionImageLoader, observeConversationRoleForUserUseCase, @@ -161,6 +167,7 @@ internal class OtherUserProfileViewModelArrangement { ) coEvery { getUserE2eiCertificateStatus.invoke(any()) } returns GetUserE2eiCertificateStatusResult.Success(CertificateStatus.VALID) coEvery { getUserE2eiCertificates.invoke(any()) } returns mapOf() + coEvery { observeLegalHoldStateForUser.invoke(any()) } returns flowOf(LegalHoldState.Disabled) } suspend fun withBlockUserResult(result: BlockUserResult) = apply { @@ -186,5 +193,9 @@ internal class OtherUserProfileViewModelArrangement { coEvery { observeUserInfo(any()) } returns flowOf(result) } + fun withLegalHoldState(result: LegalHoldState) = apply { + coEvery { observeLegalHoldStateForUser.invoke(any()) } returns flowOf(result) + } + fun arrange() = this to viewModel } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt new file mode 100644 index 00000000000..b9936d4e9d0 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt @@ -0,0 +1,141 @@ +/* + * Wire + * Copyright (C) 2024 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.userprofile.self + +import com.wire.android.config.TestDispatcherProvider +import com.wire.android.config.mockUri +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.datastore.UserDataStore +import com.wire.android.di.AuthServerConfigProvider +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.framework.TestTeam +import com.wire.android.framework.TestUser +import com.wire.android.mapper.OtherAccountMapper +import com.wire.android.notification.NotificationChannelsManager +import com.wire.android.notification.WireNotificationManager +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.ui.WireSessionImageLoader +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.legalhold.LegalHoldState +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldForSelfUserUseCase +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase +import com.wire.kalium.logic.feature.team.GetUpdatedSelfTeamUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase +import com.wire.kalium.logic.feature.user.ObserveValidAccountsUseCase +import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase +import com.wire.kalium.logic.feature.user.UpdateSelfAvailabilityStatusUseCase +import com.wire.kalium.logic.functional.Either +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.flowOf + +class SelfUserProfileViewModelArrangement { + @MockK + lateinit var userDataStore: UserDataStore + @MockK + lateinit var getSelf: GetSelfUserUseCase + @MockK + lateinit var getSelfTeam: GetUpdatedSelfTeamUseCase + @MockK + lateinit var observeValidAccounts: ObserveValidAccountsUseCase + @MockK + lateinit var updateStatus: UpdateSelfAvailabilityStatusUseCase + @MockK + lateinit var logout: LogoutUseCase + @MockK + lateinit var observeLegalHoldRequest: ObserveLegalHoldRequestUseCase + @MockK + lateinit var observeLegalHoldForSelfUser: ObserveLegalHoldForSelfUserUseCase + @MockK + lateinit var dispatchers: DispatcherProvider + @MockK + lateinit var wireSessionImageLoader: WireSessionImageLoader + @MockK + lateinit var authServerConfigProvider: AuthServerConfigProvider + @MockK + lateinit var selfServerLinks: SelfServerConfigUseCase + @MockK + lateinit var otherAccountMapper: OtherAccountMapper + @MockK + lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase + @MockK + lateinit var accountSwitch: AccountSwitchUseCase + @MockK + lateinit var endCall: EndCallUseCase + @MockK + lateinit var isReadOnlyAccount: IsReadOnlyAccountUseCase + @MockK + lateinit var notificationChannelsManager: NotificationChannelsManager + @MockK + lateinit var notificationManager: WireNotificationManager + @MockK + lateinit var globalDataStore: GlobalDataStore + @MockK + lateinit var qualifiedIdMapper: QualifiedIdMapper + + private val viewModel by lazy { + SelfUserProfileViewModel( + selfUserId = TestUser.SELF_USER.id, + dataStore = userDataStore, + getSelf = getSelf, + getSelfTeam = getSelfTeam, + observeValidAccounts = observeValidAccounts, + updateStatus = updateStatus, + logout = logout, + observeLegalHoldRequest = observeLegalHoldRequest, + observeLegalHoldForSelfUser = observeLegalHoldForSelfUser, + dispatchers = TestDispatcherProvider(), + wireSessionImageLoader = wireSessionImageLoader, + authServerConfigProvider = authServerConfigProvider, + selfServerLinks = selfServerLinks, + otherAccountMapper = otherAccountMapper, + observeEstablishedCalls = observeEstablishedCalls, + accountSwitch = accountSwitch, + endCall = endCall, + isReadOnlyAccount = isReadOnlyAccount, + notificationChannelsManager = notificationChannelsManager, + notificationManager = notificationManager, + globalDataStore = globalDataStore, + qualifiedIdMapper = qualifiedIdMapper + ) + } + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + mockUri() + + coEvery { getSelf.invoke() } returns flowOf(TestUser.SELF_USER) + coEvery { getSelfTeam.invoke() } returns Either.Right(TestTeam.TEAM) + coEvery { observeValidAccounts.invoke() } returns flowOf(listOf(TestUser.SELF_USER to TestTeam.TEAM)) + coEvery { isReadOnlyAccount.invoke() } returns false + coEvery { observeEstablishedCalls.invoke() } returns flowOf(emptyList()) + } + + fun withLegalHoldRequest(result: ObserveLegalHoldRequestUseCase.Result) = apply { + coEvery { observeLegalHoldRequest.invoke() } returns flowOf(result) + } + fun withLegalHold(result: LegalHoldState) = apply { + coEvery { observeLegalHoldForSelfUser.invoke() } returns flowOf(result) + } + fun arrange() = this to viewModel +} diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelTest.kt new file mode 100644 index 00000000000..8b6c0095d6a --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelTest.kt @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2024 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.userprofile.self + +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.NavigationTestExtension +import com.wire.android.ui.legalhold.banner.LegalHoldUIState +import com.wire.kalium.logic.feature.legalhold.LegalHoldState +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(CoroutineTestExtension::class) +@ExtendWith(NavigationTestExtension::class) +class SelfUserProfileViewModelTest { + + @Test + fun `given legal hold request available, then isUnderLegalHold is pending`() = runTest { + // given + val (_, viewModel) = SelfUserProfileViewModelArrangement() + .withLegalHold(LegalHoldState.Enabled) + .withLegalHoldRequest(ObserveLegalHoldRequestUseCase.Result.LegalHoldRequestAvailable("fingerprint".toByteArray())) + .arrange() + // then + assertEquals(LegalHoldUIState.Pending, viewModel.userProfileState.legalHoldStatus) + } + + @Test + fun `given legal hold enabled, then isUnderLegalHold is active`() = runTest { + // given + val (_, viewModel) = SelfUserProfileViewModelArrangement() + .withLegalHold(LegalHoldState.Enabled) + .withLegalHoldRequest(ObserveLegalHoldRequestUseCase.Result.NoLegalHoldRequest) + .arrange() + // then + assertEquals(LegalHoldUIState.Active, viewModel.userProfileState.legalHoldStatus) + } + + @Test + fun `given legal hold disabled and no request available, then isUnderLegalHold is none`() = runTest { + // given + val (_, viewModel) = SelfUserProfileViewModelArrangement() + .withLegalHold(LegalHoldState.Disabled) + .withLegalHoldRequest(ObserveLegalHoldRequestUseCase.Result.NoLegalHoldRequest) + .arrange() + // then + assertEquals(LegalHoldUIState.None, viewModel.userProfileState.legalHoldStatus) + } +}