From 65fc8c72697aa2bb8890c0ef635436a7dd30c218 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Fri, 18 Aug 2023 17:57:30 +0200 Subject: [PATCH 1/4] feat: create password protected conv invite link (#2099) --- .../feature/GenerateRandomPasswordUseCase.kt | 59 ++++ .../common/bottomsheet/MenuBottomSheetItem.kt | 19 +- .../ui/common/button/WireButtonDefaults.kt | 99 ++++--- .../conversations/MessageStatusIndicator.kt | 2 +- .../GroupConversationDetailsViewModel.kt | 2 +- .../details/editguestaccess/Buttons.kt | 30 +- .../CreateGuestLinkBottomSheet.kt | 96 +++++++ .../editguestaccess/EditGuestAccessScreen.kt | 93 ++++-- .../editguestaccess/EditGuestAccessState.kt | 4 +- .../EditGuestAccessViewModel.kt | 25 +- ...ionFooter.kt => GuestLinkActionButtons.kt} | 40 +-- .../PasswordProtectedLinkBanner.kt | 82 ++++++ .../CreatePasswordGuestLinkNavArgs.kt | 24 ++ .../CreatePasswordGuestLinkState.kt | 30 ++ .../CreatePasswordGuestLinkViewModel.kt | 103 +++++++ .../CreatePasswordProtectedGuestLinkScreen.kt | 239 +++++++++++++++ .../GeneratePasswordButton.kt | 65 +++++ .../ui/home/conversations/model/UIMessage.kt | 2 +- ...egraded_proteus.xml => ic_shield_holo.xml} | 0 ...essage_error.xml => ic_warning_circle.xml} | 0 app/src/main/res/values/strings.xml | 16 ++ .../GenerateRandomPasswordUseCaseTest.kt | 73 +++++ .../CreatePasswordGuestLinkViewModelText.kt | 271 ++++++++++++++++++ .../EditGuestAccessViewModelTest.kt | 16 +- kalium | 2 +- 25 files changed, 1262 insertions(+), 130 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/feature/GenerateRandomPasswordUseCase.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreateGuestLinkBottomSheet.kt rename app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/{GuestLinkActionFooter.kt => GuestLinkActionButtons.kt} (59%) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/PasswordProtectedLinkBanner.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkNavArgs.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkState.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModel.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordProtectedGuestLinkScreen.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/GeneratePasswordButton.kt rename app/src/main/res/drawable/{ic_conversation_degraded_proteus.xml => ic_shield_holo.xml} (100%) rename app/src/main/res/drawable/{ic_message_error.xml => ic_warning_circle.xml} (100%) create mode 100644 app/src/test/kotlin/com/wire/android/feature/GenerateRandomPasswordUseCaseTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreatePasswordGuestLinkViewModelText.kt diff --git a/app/src/main/kotlin/com/wire/android/feature/GenerateRandomPasswordUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/GenerateRandomPasswordUseCase.kt new file mode 100644 index 00000000000..8026ff8c61c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/feature/GenerateRandomPasswordUseCase.kt @@ -0,0 +1,59 @@ +/* + * 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.feature + +import androidx.annotation.VisibleForTesting +import dagger.hilt.android.scopes.ViewModelScoped +import java.security.SecureRandom +import javax.inject.Inject + +@ViewModelScoped +class GenerateRandomPasswordUseCase @Inject constructor() { + + operator fun invoke(): String { + + val secureRandom = SecureRandom() + + val passwordLength = secureRandom.nextInt(MAX_LENGTH - MIN_LENGTH + 1) + MIN_LENGTH + + return buildList { + add(lowercase[secureRandom.nextInt(lowercase.size)]) + add(uppercase[secureRandom.nextInt(uppercase.size)]) + add(digits[secureRandom.nextInt(digits.size)]) + add(specialChars[secureRandom.nextInt(specialChars.size)]) + + repeat(passwordLength - FIXED_CHAR_COUNT) { + add(allCharacters[secureRandom.nextInt(allCharacters.size)]) + } + }.shuffled(secureRandom).joinToString("") + } + + @VisibleForTesting + companion object { + val lowercase: List = ('a'..'z').shuffled() + val uppercase: List = ('A'..'Z').shuffled() + val digits: List = ('0'..'9').shuffled() + val specialChars: List = "!@#$%^&*()_+[]{}|;:,.<>?-".toList().shuffled() + + val allCharacters: List = (lowercase + uppercase + digits + specialChars).shuffled() + + const val MIN_LENGTH = 15 + const val MAX_LENGTH = 20 + const val FIXED_CHAR_COUNT = 4 + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt index 41f45c702a7..f56f044ee17 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt @@ -57,14 +57,21 @@ import io.github.esentsov.PackagePrivate @Composable fun MenuBottomSheetItem( title: String, - icon: @Composable () -> Unit, + icon: (@Composable () -> Unit)? = null, action: (@Composable () -> Unit)? = null, clickBlockParams: ClickBlockParams = ClickBlockParams(), itemProvidedColor: Color = MaterialTheme.colorScheme.secondary, - onItemClick: () -> Unit = {} + onItemClick: () -> Unit = {}, + enabled: Boolean = true, ) { CompositionLocalProvider(LocalContentColor provides itemProvidedColor) { - val clickable = remember(onItemClick, clickBlockParams) { Clickable(clickBlockParams = clickBlockParams, onClick = onItemClick) } + val clickable = remember(onItemClick, clickBlockParams) { + Clickable( + clickBlockParams = clickBlockParams, + onClick = onItemClick, + enabled = enabled + ) + } Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -73,8 +80,10 @@ fun MenuBottomSheetItem( .clickable(clickable) .padding(MaterialTheme.wireDimensions.conversationBottomSheetItemPadding) ) { - icon() - Spacer(modifier = Modifier.width(12.dp)) + if (icon != null) { + icon() + Spacer(modifier = Modifier.width(12.dp)) + } MenuItemTitle(title = title) if (action != null) { Spacer(modifier = Modifier.width(MaterialTheme.wireDimensions.spacing12x)) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/button/WireButtonDefaults.kt b/app/src/main/kotlin/com/wire/android/ui/common/button/WireButtonDefaults.kt index 583fb2f714a..2184dba0f52 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/button/WireButtonDefaults.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/button/WireButtonDefaults.kt @@ -56,23 +56,40 @@ fun wireSendPrimaryButtonColors() = wirePrimaryButtonColors().copy( ) @Composable -fun wireSecondaryButtonColors() = wireButtonColors( - enabled = MaterialTheme.wireColorScheme.secondaryButtonEnabled, - onEnabled = MaterialTheme.wireColorScheme.onSecondaryButtonEnabled, - enabledOutline = MaterialTheme.wireColorScheme.secondaryButtonEnabledOutline, - disabled = MaterialTheme.wireColorScheme.secondaryButtonDisabled, - onDisabled = MaterialTheme.wireColorScheme.onSecondaryButtonDisabled, - disabledOutline = MaterialTheme.wireColorScheme.secondaryButtonDisabledOutline, - selected = MaterialTheme.wireColorScheme.secondaryButtonSelected, - onSelected = MaterialTheme.wireColorScheme.onSecondaryButtonSelected, - selectedOutline = MaterialTheme.wireColorScheme.secondaryButtonSelectedOutline, - error = MaterialTheme.wireColorScheme.secondaryButtonEnabled, - onError = MaterialTheme.wireColorScheme.error, - errorOutline = MaterialTheme.wireColorScheme.secondaryButtonEnabledOutline, - positive = MaterialTheme.wireColorScheme.secondaryButtonEnabled, - onPositive = MaterialTheme.wireColorScheme.positive, - positiveOutline = MaterialTheme.wireColorScheme.secondaryButtonEnabledOutline, - ripple = MaterialTheme.wireColorScheme.secondaryButtonRipple +fun wireSecondaryButtonColors( + enabled: Color = MaterialTheme.wireColorScheme.secondaryButtonEnabled, + onEnabled: Color = MaterialTheme.wireColorScheme.onSecondaryButtonEnabled, + enabledOutline: Color = MaterialTheme.wireColorScheme.secondaryButtonEnabledOutline, + disabled: Color = MaterialTheme.wireColorScheme.secondaryButtonDisabled, + onDisabled: Color = MaterialTheme.wireColorScheme.onSecondaryButtonDisabled, + disabledOutline: Color = MaterialTheme.wireColorScheme.secondaryButtonDisabledOutline, + selected: Color = MaterialTheme.wireColorScheme.secondaryButtonSelected, + onSelected: Color = MaterialTheme.wireColorScheme.onSecondaryButtonSelected, + selectedOutline: Color = MaterialTheme.wireColorScheme.secondaryButtonSelectedOutline, + error: Color = MaterialTheme.wireColorScheme.secondaryButtonEnabled, + onError: Color = MaterialTheme.wireColorScheme.error, + errorOutline: Color = MaterialTheme.wireColorScheme.secondaryButtonEnabledOutline, + positive: Color = MaterialTheme.wireColorScheme.secondaryButtonEnabled, + onPositive: Color = MaterialTheme.wireColorScheme.positive, + positiveOutline: Color = MaterialTheme.wireColorScheme.secondaryButtonEnabledOutline, + ripple: Color = MaterialTheme.wireColorScheme.secondaryButtonRipple +) = wireButtonColors( + enabled = enabled, + onEnabled = onEnabled, + enabledOutline = enabledOutline, + disabled = disabled, + onDisabled = onDisabled, + disabledOutline = disabledOutline, + selected = selected, + onSelected = onSelected, + selectedOutline = selectedOutline, + error = error, + onError = onError, + errorOutline = errorOutline, + positive = positive, + onPositive = onPositive, + positiveOutline = positiveOutline, + ripple = ripple ) @Composable @@ -97,34 +114,44 @@ fun wireTertiaryButtonColors() = wireButtonColors( @Composable private fun wireButtonColors( - enabled: Color, onEnabled: Color, enabledOutline: Color, - disabled: Color, onDisabled: Color, disabledOutline: Color, - selected: Color, onSelected: Color, selectedOutline: Color, - error: Color, onError: Color, errorOutline: Color, - positive: Color, onPositive: Color, positiveOutline: Color, + enabled: Color, onEnabled: Color, enabledOutline: Color, + disabled: Color, onDisabled: Color, disabledOutline: Color, + selected: Color, onSelected: Color, selectedOutline: Color, + error: Color, onError: Color, errorOutline: Color, + positive: Color, onPositive: Color, positiveOutline: Color, ripple: Color ) = WireButtonColors( - enabled, onEnabled, enabledOutline, - disabled, onDisabled, disabledOutline, - selected, onSelected, selectedOutline, - error, onError, errorOutline, - positive, onPositive, positiveOutline, + enabled, onEnabled, enabledOutline, + disabled, onDisabled, disabledOutline, + selected, onSelected, selectedOutline, + error, onError, errorOutline, + positive, onPositive, positiveOutline, ripple ) @Stable data class WireButtonColors( - val enabled: Color, val onEnabled: Color, val enabledOutline: Color, - val disabled: Color, val onDisabled: Color, val disabledOutline: Color, - val selected: Color, val onSelected: Color, val selectedOutline: Color, - val error: Color, val onError: Color, val errorOutline: Color, - val positive: Color, val onPositive: Color, val positiveOutline: Color, + val enabled: Color, + val onEnabled: Color, + val enabledOutline: Color, + val disabled: Color, + val onDisabled: Color, + val disabledOutline: Color, + val selected: Color, + val onSelected: Color, + val selectedOutline: Color, + val error: Color, + val onError: Color, + val errorOutline: Color, + val positive: Color, + val onPositive: Color, + val positiveOutline: Color, val ripple: Color ) { @Composable fun containerColor(state: WireButtonState, interactionSource: InteractionSource): State = animateColorAsState( - when(state) { + when (state) { WireButtonState.Default -> enabled WireButtonState.Disabled -> disabled WireButtonState.Selected -> selected @@ -134,8 +161,8 @@ data class WireButtonColors( ) @Composable - fun outlineColor(state: WireButtonState, interactionSource: InteractionSource): State = animateColorAsState( - when(state) { + fun outlineColor(state: WireButtonState, interactionSource: InteractionSource): State = animateColorAsState( + when (state) { WireButtonState.Default -> enabledOutline WireButtonState.Disabled -> disabledOutline WireButtonState.Selected -> selectedOutline @@ -146,7 +173,7 @@ data class WireButtonColors( @Composable fun contentColor(state: WireButtonState, interactionSource: InteractionSource): State = animateColorAsState( - when(state) { + when (state) { WireButtonState.Default -> onEnabled WireButtonState.Disabled -> onDisabled WireButtonState.Selected -> onSelected diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageStatusIndicator.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageStatusIndicator.kt index 27dda198add..f545b4981fc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageStatusIndicator.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageStatusIndicator.kt @@ -87,7 +87,7 @@ fun MessageStatusIndicator( is MessageFlowStatus.Failure -> Icon( modifier = modifier, - painter = painterResource(id = R.drawable.ic_message_error), + painter = painterResource(id = R.drawable.ic_warning_circle), tint = MaterialTheme.wireColorScheme.error, contentDescription = stringResource(R.string.content_description_message_error_status), ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt index df214db4c3c..afdfdff645d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt @@ -93,7 +93,7 @@ class GroupConversationDetailsViewModel @Inject constructor( private val observeSelfDeletionTimerSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, override val savedStateHandle: SavedStateHandle, private val isMLSEnabled: IsMLSEnabledUseCase, - private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, + refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, ) : GroupConversationParticipantsViewModel( savedStateHandle, observeConversationMembers, refreshUsersWithoutMetadata ), GroupConversationDetailsBottomSheetEventsHandler { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/Buttons.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/Buttons.kt index 8f9452752ed..5c8e257605c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/Buttons.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/Buttons.kt @@ -20,7 +20,6 @@ package com.wire.android.ui.home.conversations.details.editguestaccess -import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -30,27 +29,25 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.wire.android.R import com.wire.android.ui.common.button.WireButtonState -import com.wire.android.ui.common.button.WirePrimaryButton -import com.wire.android.ui.common.button.wirePrimaryButtonColors -import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.button.wireSecondaryButtonColors import com.wire.android.ui.theme.wireDimensions @Composable -fun CreateLinkButton( - shouldDisableGenerateGuestLinkButton: Boolean, +fun CreateGuestLinkButton( + enabled: Boolean, isLoading: Boolean, onCreateLink: () -> Unit ) { - WirePrimaryButton( + WireSecondaryButton( text = stringResource(id = R.string.guest_link_button_create_link), fillMaxWidth = true, onClick = onCreateLink, loading = isLoading, - state = if (shouldDisableGenerateGuestLinkButton) WireButtonState.Disabled + state = if (!enabled) WireButtonState.Disabled else WireButtonState.Default, modifier = Modifier .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) .padding(MaterialTheme.wireDimensions.spacing16x) ) @@ -60,10 +57,9 @@ fun CreateLinkButton( fun CopyLinkButton( onCopy: () -> Unit ) { - WirePrimaryButton( + WireSecondaryButton( modifier = Modifier .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) .padding( start = MaterialTheme.wireDimensions.spacing16x, end = MaterialTheme.wireDimensions.spacing16x, @@ -77,10 +73,9 @@ fun CopyLinkButton( fun ShareLinkButton( onShare: () -> Unit ) { - WirePrimaryButton( + WireSecondaryButton( modifier = Modifier .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) .padding( start = MaterialTheme.wireDimensions.spacing16x, end = MaterialTheme.wireDimensions.spacing16x, @@ -96,21 +91,20 @@ fun RevokeLinkButton( isLoading: Boolean = false, onRevoke: () -> Unit ) { - WirePrimaryButton( + WireSecondaryButton( modifier = Modifier .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) .padding( start = MaterialTheme.wireDimensions.spacing16x, end = MaterialTheme.wireDimensions.spacing16x, top = MaterialTheme.wireDimensions.spacing4x, bottom = MaterialTheme.wireDimensions.spacing12x ), - colors = wirePrimaryButtonColors().copy(enabled = colorsScheme().error), + colors = wireSecondaryButtonColors(), text = stringResource(id = R.string.guest_link_button_revoke_link), fillMaxWidth = true, loading = isLoading, - state = if (isLoading) WireButtonState.Disabled else WireButtonState.Default, + state = if (isLoading) WireButtonState.Disabled else WireButtonState.Error, onClick = onRevoke ) } @@ -136,5 +130,5 @@ fun PreviewShareLinkButton() { @Preview @Composable fun PreviewCreateLinkButton() { - CreateLinkButton(true, false) {} + CreateGuestLinkButton(true, false) {} } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreateGuestLinkBottomSheet.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreateGuestLinkBottomSheet.kt new file mode 100644 index 00000000000..a642b38b50e --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreateGuestLinkBottomSheet.kt @@ -0,0 +1,96 @@ +/* + * 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.details.editguestaccess + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.wire.android.R +import com.wire.android.ui.common.ArrowRightIcon +import com.wire.android.ui.common.bottomsheet.MenuBottomSheetItem +import com.wire.android.ui.common.bottomsheet.MenuModalSheetContent +import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader +import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout +import com.wire.android.ui.common.bottomsheet.WireModalSheetState + +@Composable +fun CreateGuestLinkBottomSheet( + sheetState: WireModalSheetState, + onItemClick: (passwordProtected: Boolean) -> Unit, + isPasswordInviteLinksAllowed: Boolean, +) { + val coroutineScope = rememberCoroutineScope() + WireModalSheetLayout(sheetState = sheetState, coroutineScope = coroutineScope) { + MenuModalSheetContent( + header = MenuModalSheetHeader.Visible(title = stringResource(R.string.create_guest_link)), + menuItems = buildList { + add { + CreateInviteLinkSheetItem( + title = stringResource(R.string.create_guest_link_with_password), + onClicked = { onItemClick(true) }, + enabled = isPasswordInviteLinksAllowed + ) + } + add { + CreateInviteLinkSheetItem( + title = stringResource(R.string.create_guest_link_without_password_title), + onClicked = { onItemClick(false) }, + enabled = true + ) + } + } + ) + } +} + +@Composable +private fun CreateInviteLinkSheetItem( + title: String, + onClicked: () -> Unit, + enabled: Boolean = true, +) { + MenuBottomSheetItem( + title = title, + onItemClick = onClicked, + action = { + ArrowRightIcon() + }, + enabled = enabled + ) +} + +@Preview +@Composable +fun PreviewCreateGuestLinkBottomSheet() { + CreateGuestLinkBottomSheet( + sheetState = WireModalSheetState(), + onItemClick = {}, + isPasswordInviteLinksAllowed = true, + ) +} + +@Preview +@Composable +fun PreviewCreateGuestLinkBottomSheetDisabled() { + CreateGuestLinkBottomSheet( + sheetState = WireModalSheetState(), + onItemClick = {}, + isPasswordInviteLinksAllowed = false, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt index ade32e890fd..b46b2f89a56 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt @@ -22,18 +22,23 @@ package com.wire.android.ui.home.conversations.details.editguestaccess import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize 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.rememberScrollState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetValue import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext @@ -43,19 +48,26 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R +import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.rememberNavigator +import com.wire.android.ui.common.bottomsheet.WireModalSheetState import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.rememberTopBarElevationState import com.wire.android.ui.common.snackbar.SwipeDismissSnackbarHost import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar +import com.wire.android.ui.destinations.CreatePasswordProtectedGuestLinkScreenDestination +import com.wire.android.ui.home.conversations.details.editguestaccess.createPasswordProtectedGuestLink.CreatePasswordGuestLinkNavArgs import com.wire.android.ui.home.conversationslist.common.FolderHeader 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.copyLinkToClipboard import com.wire.android.util.shareViaIntent +import kotlinx.coroutines.launch +@Suppress("ComplexMethod") +@OptIn(ExperimentalMaterial3Api::class) @RootNavGraph @Destination( navArgsDelegate = EditGuestAccessNavArgs::class @@ -67,6 +79,33 @@ fun EditGuestAccessScreen( ) { val scrollState = rememberScrollState() val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + val sheetState = remember { + WireModalSheetState(SheetValue.Hidden) + } + val onSheetItemClick: (Boolean) -> Unit = remember { + { isPasswordProtected -> + coroutineScope.launch { sheetState.hide() } + if (isPasswordProtected) { + navigator.navigate( + NavigationCommand( + CreatePasswordProtectedGuestLinkScreenDestination( + CreatePasswordGuestLinkNavArgs( + conversationId = editGuestAccessViewModel.conversationId + ) + ) + ) + ) + } else { + editGuestAccessViewModel.onRequestGuestRoomLink() + } + } + } + CreateGuestLinkBottomSheet( + sheetState = sheetState, + onSheetItemClick, + isPasswordInviteLinksAllowed = true, + ) Scaffold(topBar = { WireCenterAlignedTopAppBar( @@ -125,7 +164,12 @@ fun EditGuestAccessScreen( color = MaterialTheme.wireColorScheme.secondaryText, modifier = Modifier.padding(top = MaterialTheme.wireDimensions.spacing2x) ) - editGuestAccessState.link?.let { + Spacer(modifier = Modifier.height(dimensions().spacing16x)) + + editGuestAccessState.link?.also { + if (editGuestAccessState.isLinkPasswordProtected) { + PasswordProtectedLinkBanner() + } Text( text = it, style = MaterialTheme.wireTypography.body01, @@ -135,31 +179,32 @@ fun EditGuestAccessScreen( } } } - } - - val clipboardManager = LocalClipboardManager.current - val context = LocalContext.current + item { + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current - with(editGuestAccessViewModel) { - GuestLinkActionFooter( - shouldDisableGenerateGuestLinkButton = shouldDisableGenerateGuestLinkButton(), - isGeneratingLink = editGuestAccessState.isGeneratingGuestRoomLink, - isRevokingLink = editGuestAccessState.isRevokingLink, - link = editGuestAccessState.link, - onCreateLink = ::onGenerateGuestRoomLink, - onRevokeLink = ::onRevokeGuestRoomLink, - onCopyLink = { - editGuestAccessState = editGuestAccessState.copy(isLinkCopied = true) - editGuestAccessState.link?.let { - clipboardManager.copyLinkToClipboard(it) - } - }, - onShareLink = { - editGuestAccessState.link?.let { - context.shareViaIntent(it) - } + with(editGuestAccessViewModel) { + GuestLinkActionButtons( + shouldDisableGenerateGuestLinkButton = shouldDisableGenerateGuestLinkButton(), + isGeneratingLink = editGuestAccessState.isGeneratingGuestRoomLink, + isRevokingLink = editGuestAccessState.isRevokingLink, + link = editGuestAccessState.link, + onCreateLink = sheetState::show, + onRevokeLink = ::onRevokeGuestRoomLink, + onCopyLink = { + editGuestAccessState = editGuestAccessState.copy(isLinkCopied = true) + editGuestAccessState.link?.let { + clipboardManager.copyLinkToClipboard(it) + } + }, + onShareLink = { + editGuestAccessState.link?.let { + context.shareViaIntent(it) + } + } + ) } - ) + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessState.kt index 943c8882df9..66d27c891f3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessState.kt @@ -33,5 +33,7 @@ data class EditGuestAccessState( val isRevokingLink: Boolean = false, val isLinkCopied: Boolean = false, val isFailedToRevokeGuestRoomLink: Boolean = false, - val link: String? = null + val link: String? = null, + val isLinkPasswordProtected: Boolean = false, + val shouldShowPasswordDialog: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModel.kt index 3a232bd9c8a..14eaf9aaeeb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModel.kt @@ -39,6 +39,7 @@ import com.wire.kalium.logic.feature.conversation.guestroomlink.ObserveGuestRoom import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkResult import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkUseCase import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase +import com.wire.kalium.logic.functional.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -160,6 +161,7 @@ class EditGuestAccessViewModel @Inject constructor( isGuestAccessAllowed = !shouldEnableGuestAccess ) ) + is UpdateConversationAccessRoleUseCase.Result.Success -> Unit } updateState(editGuestAccessState.copy(isUpdatingGuestAccess = false)) @@ -167,14 +169,18 @@ class EditGuestAccessViewModel @Inject constructor( } } - fun onGenerateGuestRoomLink() { + fun onRequestGuestRoomLink() { viewModelScope.launch { - editGuestAccessState = editGuestAccessState.copy(isGeneratingGuestRoomLink = true) - generateGuestRoomLink(conversationId).also { - editGuestAccessState = editGuestAccessState.copy(isGeneratingGuestRoomLink = false) - if (it is GenerateGuestRoomLinkResult.Failure) { - editGuestAccessState = editGuestAccessState.copy(isFailedToGenerateGuestRoomLink = true) - } + safeCreateGuestLink(null) + } + } + + private suspend fun safeCreateGuestLink(password: String?) { + editGuestAccessState = editGuestAccessState.copy(isGeneratingGuestRoomLink = true) + generateGuestRoomLink(conversationId, password).also { + editGuestAccessState = editGuestAccessState.copy(isGeneratingGuestRoomLink = false) + if (it is GenerateGuestRoomLinkResult.Failure) { + editGuestAccessState = editGuestAccessState.copy(isFailedToGenerateGuestRoomLink = true) } } } @@ -221,7 +227,10 @@ class EditGuestAccessViewModel @Inject constructor( private fun startObservingGuestRoomLink() { viewModelScope.launch { observeGuestRoomLink(conversationId).collect { - editGuestAccessState = editGuestAccessState.copy(link = it) + it.onSuccess { + editGuestAccessState = + editGuestAccessState.copy(link = it?.link, isLinkPasswordProtected = it?.isPasswordProtected ?: false) + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/GuestLinkActionFooter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/GuestLinkActionButtons.kt similarity index 59% rename from app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/GuestLinkActionFooter.kt rename to app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/GuestLinkActionButtons.kt index f98946515f0..3f38f2416d5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/GuestLinkActionFooter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/GuestLinkActionButtons.kt @@ -20,18 +20,12 @@ package com.wire.android.ui.home.conversations.details.editguestaccess -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.wire.android.ui.common.dimensions -import com.wire.android.ui.theme.wireColorScheme @Composable -fun GuestLinkActionFooter( +fun GuestLinkActionButtons( shouldDisableGenerateGuestLinkButton: Boolean, isGeneratingLink: Boolean, isRevokingLink: Boolean, @@ -42,23 +36,17 @@ fun GuestLinkActionFooter( onShareLink: () -> Unit ) { - Surface( - modifier = Modifier - .background(MaterialTheme.wireColorScheme.background), - shadowElevation = dimensions().spacing8x - ) { - if (link.isNullOrEmpty()) { - CreateLinkButton( - shouldDisableGenerateGuestLinkButton = shouldDisableGenerateGuestLinkButton, - isLoading = isGeneratingLink, - onCreateLink = onCreateLink - ) - } else { - Column { - CopyLinkButton(onCopyLink) - ShareLinkButton(onShareLink) - RevokeLinkButton(isLoading = isRevokingLink, onRevoke = onRevokeLink) - } + if (link.isNullOrEmpty()) { + CreateGuestLinkButton( + enabled = !shouldDisableGenerateGuestLinkButton, + isLoading = isGeneratingLink, + onCreateLink = onCreateLink + ) + } else { + Column { + CopyLinkButton(onCopyLink) + ShareLinkButton(onShareLink) + RevokeLinkButton(isLoading = isRevokingLink, onRevoke = onRevokeLink) } } } @@ -66,11 +54,11 @@ fun GuestLinkActionFooter( @Preview @Composable fun PreviewLinkSection() { - GuestLinkActionFooter( + GuestLinkActionButtons( shouldDisableGenerateGuestLinkButton = false, isGeneratingLink = false, isRevokingLink = false, - link = "", + link = "123", onCreateLink = {}, onRevokeLink = {}, onCopyLink = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/PasswordProtectedLinkBanner.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/PasswordProtectedLinkBanner.kt new file mode 100644 index 00000000000..c3a07e72cee --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/PasswordProtectedLinkBanner.kt @@ -0,0 +1,82 @@ +/* + * 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.details.editguestaccess + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.wire.android.R +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireTypography + +@Composable +fun PasswordProtectedLinkBanner() { + Row( + modifier = Modifier + .padding(top = 16.dp, end = 16.dp, bottom = 16.dp) + .height(IntrinsicSize.Min) + ) { + Divider( + color = colorsScheme().outline, + modifier = Modifier + .fillMaxHeight() + .width(2.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + + Column { + Row( + modifier = Modifier.wrapContentHeight() + ) { + Text( + text = stringResource(id = R.string.password_protected_link_banner_title), + style = MaterialTheme.wireTypography.title02, + ) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_shield_holo), + modifier = Modifier + .width(16.dp) + .height(16.dp), + contentDescription = null + ) + } + Text( + text = stringResource(id = R.string.password_protected_link_banner_description), + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.secondaryText, + ) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkNavArgs.kt new file mode 100644 index 00000000000..6551b02f312 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkNavArgs.kt @@ -0,0 +1,24 @@ +/* + * 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.details.editguestaccess.createPasswordProtectedGuestLink + +import com.wire.kalium.logic.data.id.ConversationId + +data class CreatePasswordGuestLinkNavArgs( + val conversationId: ConversationId +) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkState.kt new file mode 100644 index 00000000000..02228149a4b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkState.kt @@ -0,0 +1,30 @@ +/* + * 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.details.editguestaccess.createPasswordProtectedGuestLink + +import androidx.compose.ui.text.input.TextFieldValue +import com.wire.kalium.logic.CoreFailure + +data class CreatePasswordGuestLinkState( + val password: TextFieldValue = TextFieldValue(""), + val passwordConfirm: TextFieldValue = TextFieldValue(""), + val isLoading: Boolean = false, + val error: CoreFailure? = null, + val isPasswordValid: Boolean = false, + val isLinkCreationSuccessful: Boolean = false +) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModel.kt new file mode 100644 index 00000000000..b8791b1be21 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModel.kt @@ -0,0 +1,103 @@ +/* + * 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.details.editguestaccess.createPasswordProtectedGuestLink + +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.feature.GenerateRandomPasswordUseCase +import com.wire.android.ui.navArgs +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkResult +import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CreatePasswordGuestLinkViewModel @Inject constructor( + private val generateGuestRoomLink: GenerateGuestRoomLinkUseCase, + private val validatePassword: ValidatePasswordUseCase, + private val generateRandomPasswordUseCase: GenerateRandomPasswordUseCase, + savedStateHandle: SavedStateHandle +) : ViewModel() { + + private val editGuestAccessNavArgs: CreatePasswordGuestLinkNavArgs = savedStateHandle.navArgs() + private val conversationId: QualifiedID = editGuestAccessNavArgs.conversationId + + var state by mutableStateOf(CreatePasswordGuestLinkState()) + @VisibleForTesting set + + fun onGenerateLink() { + state = state.copy(isLoading = true) + viewModelScope.launch { + generateGuestRoomLink( + conversationId = conversationId, + password = state.password.text + ).also { result -> + state = if (result is GenerateGuestRoomLinkResult.Failure) { + state.copy(error = result.cause, isLoading = false) + } else { + state.copy(error = null, isLoading = false, isLinkCreationSuccessful = true) + } + } + } + } + + fun onPasswordUpdated(password: TextFieldValue) { + if (password.text != state.password.text) { + state = state.copy(password = password) + checkIfPasswordIsValidAndConfirmed() + } else { + state = state.copy(password = password) + } + } + + fun onPasswordConfirmUpdated(password: TextFieldValue) { + if (password.text != state.passwordConfirm.text) { + state = state.copy(passwordConfirm = password) + checkIfPasswordIsValidAndConfirmed() + } else { + state = state.copy(passwordConfirm = password) + } + } + + private fun checkIfPasswordIsValidAndConfirmed() { + state = if (validatePassword(state.password.text) && state.password.text == state.passwordConfirm.text) { + state.copy(isPasswordValid = true) + } else { + state.copy(isPasswordValid = false) + } + } + + fun onErrorDialogDismissed() { + state = state.copy(error = null) + } + + fun onGenerateRandomPassword() { + val password = generateRandomPasswordUseCase() + state = state.copy(password = TextFieldValue(password), passwordConfirm = TextFieldValue(password)) + checkIfPasswordIsValidAndConfirmed() + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordProtectedGuestLinkScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordProtectedGuestLinkScreen.kt new file mode 100644 index 00000000000..a38db4c3400 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordProtectedGuestLinkScreen.kt @@ -0,0 +1,239 @@ +/* + * 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.details.editguestaccess.createPasswordProtectedGuestLink + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.rememberScrollState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.R +import com.wire.android.navigation.Navigator +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.rememberTopBarElevationState +import com.wire.android.ui.common.snackbar.SwipeDismissSnackbarHost +import com.wire.android.ui.common.textfield.WirePasswordTextField +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar +import com.wire.android.ui.home.conversations.details.editguestaccess.GenerateGuestRoomLinkFailureDialog +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireDimensions +import com.wire.android.ui.theme.wireTypography + +@OptIn(ExperimentalComposeUiApi::class) +@RootNavGraph +@Destination( + navArgsDelegate = CreatePasswordGuestLinkNavArgs::class +) +@Composable +fun CreatePasswordProtectedGuestLinkScreen( + navigator: Navigator, + viewModel: CreatePasswordGuestLinkViewModel = hiltViewModel(), +) { + val scrollState = rememberScrollState() + val snackbarHostState = remember { SnackbarHostState() } + val context = LocalContext.current + val clipboardManager = LocalClipboardManager.current + val onCopyClick = remember(viewModel.state.password.text) { + { + if (viewModel.state.isPasswordValid) { + clipboardManager.setText(viewModel.state.password.annotatedString) + Toast.makeText( + context, + context.getString(R.string.conversation_options_create_password_protected_guest_link_password_copied), + Toast.LENGTH_SHORT + ).show() + } + } + } + LaunchedEffect(viewModel.state.isLinkCreationSuccessful) { + if (viewModel.state.isLinkCreationSuccessful) { + onCopyClick() + navigator.navigateBack() + } + } + + Scaffold(topBar = { + WireCenterAlignedTopAppBar( + elevation = scrollState.rememberTopBarElevationState().value, + onNavigationPressed = navigator::navigateBack, + title = stringResource(id = R.string.conversation_options_create_password_protected_guest_link_title), + ) + }, snackbarHost = { + SwipeDismissSnackbarHost( + hostState = snackbarHostState, modifier = Modifier.fillMaxWidth() + ) + }) { internalPadding -> + Column { + LazyColumn( + modifier = Modifier + .background(MaterialTheme.wireColorScheme.background) + .padding(internalPadding) + .padding( + start = dimensions().spacing16x, + end = dimensions().spacing16x, + bottom = dimensions().spacing16x, + top = dimensions().spacing16x, + ) + .weight(1F) + .fillMaxSize() + ) { + + item { + Text( + text = stringResource( + id = R.string.conversation_options_create_password_protected_guest_link_discrption + ), + style = MaterialTheme.wireTypography.body02.copy(fontWeight = FontWeight.Normal) + ) + Spacer(modifier = Modifier.height(dimensions().spacing16x)) + Text( + text = stringResource( + id = R.string.conversation_options_create_password_protected_guest_link_discrption_2 + ), + style = MaterialTheme.wireTypography.body02 + ) + Spacer(modifier = Modifier.height(dimensions().spacing24x)) + } + item { + val onClick = remember(viewModel.state.password.text) { + { + viewModel.onGenerateRandomPassword() + Toast.makeText( + context, + context.getString(R.string.conversation_options_create_password_protected_guest_link_password_generated), + Toast.LENGTH_SHORT + ).show() + } + } + GeneratePasswordButton( + onClick = onClick + ) + Spacer(modifier = Modifier.height(dimensions().spacing24x)) + } + item { + WirePasswordTextField( + labelText = stringResource( + id = R.string.conversation_options_create_password_protected_guest_link_password_label + ), + value = viewModel.state.password, + placeholderText = stringResource( + id = R.string.conversation_options_create_password_protected_guest_link_button_placeholder_text + ), + onValueChange = viewModel::onPasswordUpdated, + autofillTypes = emptyList() + ) + Spacer(modifier = Modifier.height(dimensions().spacing8x)) + } + item { + + Text( + style = MaterialTheme.wireTypography.subline01, + text = stringResource( + id = R.string.conversation_options_create_password_protected_guest_link_password_description + ) + ) + Spacer(modifier = Modifier.height(dimensions().spacing16x)) + } + item { + WirePasswordTextField( + labelText = stringResource( + id = R.string.conversation_options_create_confirm_password_protected_guest_link_password_label + ), + placeholderText = stringResource( + id = R.string.conversation_options_create_password_protected_guest_link_button_placeholder_text + ), + value = viewModel.state.passwordConfirm, + onValueChange = viewModel::onPasswordConfirmUpdated, + autofillTypes = emptyList() + ) + Spacer(modifier = Modifier.height(dimensions().spacing24x)) + } + } + Surface( + modifier = Modifier + .background(MaterialTheme.wireColorScheme.background), + shadowElevation = dimensions().spacing8x + ) { + CreateButton( + enabled = viewModel.state.isPasswordValid, + isLoading = viewModel.state.isLoading, + onCreateLink = viewModel::onGenerateLink + ) + } + } + + if (viewModel.state.error != null) { + GenerateGuestRoomLinkFailureDialog( + onDismiss = viewModel::onErrorDialogDismissed + ) + } + } +} + +@Composable +private fun CreateButton( + enabled: Boolean, + isLoading: Boolean, + onCreateLink: () -> Unit +) { + WirePrimaryButton( + text = stringResource(id = R.string.guest_link_button_create_link), + fillMaxWidth = true, + onClick = onCreateLink, + loading = isLoading, + state = if (!enabled) WireButtonState.Disabled + else WireButtonState.Default, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.wireColorScheme.background) + .padding(MaterialTheme.wireDimensions.spacing16x) + + ) +} + +@Preview +@Composable +fun PreviewCreatePasswordProtectedGuestLinkScreen() { + CreatePasswordProtectedGuestLinkScreen(navigator = Navigator(finish = {}, navController = NavHostController(LocalContext.current))) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/GeneratePasswordButton.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/GeneratePasswordButton.kt new file mode 100644 index 00000000000..744d3c096d0 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/GeneratePasswordButton.kt @@ -0,0 +1,65 @@ +/* + * 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.details.editguestaccess.createPasswordProtectedGuestLink + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +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.common.button.IconAlignment +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.wireTypography + +@Composable +fun GeneratePasswordButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + + WireSecondaryButton( + modifier = modifier, + onClick = onClick, + minHeight = 16.dp, + state = WireButtonState.Default, + fillMaxWidth = false, + textStyle = MaterialTheme.wireTypography.button03, + leadingIcon = { + Icon( + modifier = Modifier.padding(end = dimensions().corner4x), + painter = painterResource(id = R.drawable.ic_shield_holo), + contentDescription = null + ) + }, + leadingIconAlignment = IconAlignment.Center, + text = stringResource(id = R.string.generate_password_button_text) + ) +} + +@Preview +@Composable +fun GeneratePasswordButtonPreview() { + GeneratePasswordButton(onClick = {}) +} 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 b6d9571924d..37ffd966269 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 @@ -441,7 +441,7 @@ sealed class UIMessageContent { data class ConversationDegraded(val protocol: Conversation.Protocol) : SystemMessage( if (protocol == Conversation.Protocol.MLS) R.drawable.ic_conversation_degraded_mls - else R.drawable.ic_conversation_degraded_proteus, + else R.drawable.ic_shield_holo, R.string.label_system_message_conversation_degraded ) } diff --git a/app/src/main/res/drawable/ic_conversation_degraded_proteus.xml b/app/src/main/res/drawable/ic_shield_holo.xml similarity index 100% rename from app/src/main/res/drawable/ic_conversation_degraded_proteus.xml rename to app/src/main/res/drawable/ic_shield_holo.xml diff --git a/app/src/main/res/drawable/ic_message_error.xml b/app/src/main/res/drawable/ic_warning_circle.xml similarity index 100% rename from app/src/main/res/drawable/ic_message_error.xml rename to app/src/main/res/drawable/ic_warning_circle.xml diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e04a750569..88f32f2ee34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1165,4 +1165,20 @@ sent an interactive message Conversation Password Enter password + Create Guest Link + Create password secured link + Create link without password + Create Password Secured Link + People who want to join the conversation via the guest link need to enter this password first. + You can’t change the password later. Make sure to copy and store it. + SET PASSWORD + CONFIRM PASSWORD + Enter password + Generate Password + People who want to join the conversation via the guest link need to enter this password first.\nForgot password? Revoke the link and create a new one. + Link is password secured + Password copied to clipboard + Use at least 8 characters, with one lowercase letter, one capital letter, a number, and a special character. + New password generated + diff --git a/app/src/test/kotlin/com/wire/android/feature/GenerateRandomPasswordUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/GenerateRandomPasswordUseCaseTest.kt new file mode 100644 index 00000000000..4be47dbfba8 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/feature/GenerateRandomPasswordUseCaseTest.kt @@ -0,0 +1,73 @@ +/* + * 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.feature + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class GenerateRandomPasswordUseCaseTest { + + @Test + fun `Given GenerateRandomPasswordUseCase, when generating a password, then it should meet the specified criteria`() { + val generateRandomPasswordUseCase = GenerateRandomPasswordUseCase() + + repeat(100) { // Run the test 100 times + val password = generateRandomPasswordUseCase.invoke() + + // Test criteria + assertTrue(password.length >= GenerateRandomPasswordUseCase.MIN_LENGTH) + assertTrue(password.length <= GenerateRandomPasswordUseCase.MAX_LENGTH) + assertTrue(password.any { it in GenerateRandomPasswordUseCase.lowercase }) + assertTrue(password.any { it in GenerateRandomPasswordUseCase.uppercase }) + assertTrue(password.any { it in GenerateRandomPasswordUseCase.digits }) + assertTrue(password.any { it in GenerateRandomPasswordUseCase.specialChars }) + } + } + + @Test + fun `Given character sets, when accessing lowercase, then it should return the expected value`() { + assertEquals("abcdefghijklmnopqrstuvwxyz".toList(), GenerateRandomPasswordUseCase.lowercase.sorted()) + } + + @Test + fun `Given character sets, when accessing uppercase, then it should return the expected value`() { + assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ".toList(), GenerateRandomPasswordUseCase.uppercase.sorted()) + } + + @Test + fun `Given character sets, when accessing digits, then it should return the expected value`() { + assertEquals("0123456789".toList(), GenerateRandomPasswordUseCase.digits.sorted()) + } + + @Test + fun `Given character sets, when accessing specialChars, then it should return the expected value`() { + assertEquals("!@#$%^&*()_+[]{}|;:,.<>?-".toList().sorted(), GenerateRandomPasswordUseCase.specialChars.sorted()) + } + + @Test + fun `Given character sets, when accessing allCharacters, then it should return the expected value`() { + val expectedAllCharacters = + GenerateRandomPasswordUseCase.lowercase + + GenerateRandomPasswordUseCase.uppercase + + GenerateRandomPasswordUseCase.digits + + GenerateRandomPasswordUseCase.specialChars + + assertEquals(expectedAllCharacters.toList().sorted(), GenerateRandomPasswordUseCase.allCharacters.sorted()) + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreatePasswordGuestLinkViewModelText.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreatePasswordGuestLinkViewModelText.kt new file mode 100644 index 00000000000..295a3fb1f77 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreatePasswordGuestLinkViewModelText.kt @@ -0,0 +1,271 @@ +/* + * 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.details.editguestaccess + +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.NavigationTestExtension +import com.wire.android.config.ScopedArgsTestExtension +import com.wire.android.feature.GenerateRandomPasswordUseCase +import com.wire.android.ui.home.conversations.details.editguestaccess.createPasswordProtectedGuestLink.CreatePasswordGuestLinkNavArgs +import com.wire.android.ui.home.conversations.details.editguestaccess.createPasswordProtectedGuestLink.CreatePasswordGuestLinkViewModel +import com.wire.android.ui.navArgs +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkResult +import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import org.amshove.kluent.internal.assertEquals +import org.amshove.kluent.internal.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +@ExtendWith(ScopedArgsTestExtension::class) +@ExtendWith(NavigationTestExtension::class) +class CreatePasswordGuestLinkViewModelText { + + @Test + fun `given onPasswordUpdated called, when password is valid and password matches confirm, then isPasswordValid is marked as true`() { + val (_, viewModel) = Arrangement() + .withPasswordValidation(true) + .arrange() + + viewModel.state = viewModel.state.copy( + password = TextFieldValue("123"), + passwordConfirm = TextFieldValue("password") + ) + + viewModel.onPasswordUpdated(TextFieldValue("password")) + + assertTrue(viewModel.state.isPasswordValid) + } + + @Test + fun `given onPasswordUpdated, when password is valid and doesn't match confirm, then isPasswordValid is marked as false`() { + val (_, viewModel) = Arrangement() + .withPasswordValidation(true) + .arrange() + + viewModel.state = viewModel.state.copy( + password = TextFieldValue("password"), + passwordConfirm = TextFieldValue("password"), + isPasswordValid = true + ) + + viewModel.onPasswordUpdated(TextFieldValue("123")) + + assertFalse(viewModel.state.isPasswordValid) + } + + @Test + fun `given onPasswordConfirmUpdated called, when the new password differ from the state, then isPasswordCopied is marked as false`() { + val (_, viewModel) = Arrangement() + .withPasswordValidation(true) + .arrange() + + viewModel.state = viewModel.state.copy( + passwordConfirm = TextFieldValue("old_password") + ) + + viewModel.onPasswordConfirmUpdated(TextFieldValue("new_password")) + + assertEquals(TextFieldValue("new_password"), viewModel.state.passwordConfirm) + } + + @Test + fun `given onPasswordConfirmUpdated, when the new password doesn't differ from the state, then isPasswordCopied is not changed`() { + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.state = viewModel.state.copy( + passwordConfirm = TextFieldValue("password") + ) + + viewModel.onPasswordConfirmUpdated(TextFieldValue("password")) + + assertEquals(TextFieldValue("password"), viewModel.state.passwordConfirm) + + verify(exactly = 0) { + arrangement.validatePassword(any()) + } + } + + @Test + fun `given onPasswordConfirmUpdated, when password is valid and matches confirm, then isPasswordValid is true`() { + val (_, viewModel) = Arrangement() + .withPasswordValidation(true) + .arrange() + + viewModel.state = viewModel.state.copy( + password = TextFieldValue("password"), + passwordConfirm = TextFieldValue("123") + ) + + viewModel.onPasswordConfirmUpdated(TextFieldValue("password")) + + assertTrue(viewModel.state.isPasswordValid) + } + + @Test + fun `given onPasswordConfirmUpdated called, when password is valid and doesn't match confirm, then isPasswordValid is false`() { + val (_, viewModel) = Arrangement() + .withPasswordValidation(true) + .arrange() + + viewModel.state = viewModel.state.copy( + password = TextFieldValue("password"), + passwordConfirm = TextFieldValue("password"), + isPasswordValid = true + ) + + viewModel.onPasswordConfirmUpdated(TextFieldValue("123")) + + assertFalse(viewModel.state.isPasswordValid) + } + + @Test + fun `given onGenerateRandomPassword called, when password is generated, then password and passwordConfirm are updated`() { + val (_, viewModel) = Arrangement() + .withGenerateRandomPassword("password") + .withPasswordValidation(true) + .arrange() + + viewModel.state = viewModel.state.copy( + password = TextFieldValue("123"), + passwordConfirm = TextFieldValue("123"), + isPasswordValid = false + ) + + viewModel.onGenerateRandomPassword() + + assertTrue(viewModel.state.password.text.isNotEmpty()) + assertTrue(viewModel.state.passwordConfirm.text.isNotEmpty()) + assertEquals(viewModel.state.password, viewModel.state.passwordConfirm) + assertTrue(viewModel.state.isPasswordValid) + } + + @Test + fun `given onGenerateLink called, when link is generated, then isLinkCreationSuccessful is marked as true`() { + val (_, viewModel) = Arrangement() + .withGenerateGuestLink( + GenerateGuestRoomLinkResult.Success + ) + .arrange() + + viewModel.state = viewModel.state.copy( + password = TextFieldValue("password"), + passwordConfirm = TextFieldValue("password"), + isPasswordValid = true + ) + + viewModel.onGenerateLink() + + assertTrue(viewModel.state.isLinkCreationSuccessful) + } + + @Test + fun `given onGenerateLink called, when link is not generated, then isLinkCreationSuccessful is marked as false`() { + val expectedError = NetworkFailure.NoNetworkConnection(null) + val (_, viewModel) = Arrangement() + .withGenerateGuestLink( + GenerateGuestRoomLinkResult.Failure(expectedError) + ) + .arrange() + + viewModel.state = viewModel.state.copy( + password = TextFieldValue("password"), + passwordConfirm = TextFieldValue("password"), + isPasswordValid = true + ) + + viewModel.onGenerateLink() + + assertFalse(viewModel.state.isLinkCreationSuccessful) + assertEquals( + expectedError, + viewModel.state.error + ) + } + + private companion object { + val CONVERSATION_ID = ConversationId("conv_id", "conv_domain") + } + + private class Arrangement { + + @MockK + lateinit var savedStateHandle: SavedStateHandle + + @MockK + lateinit var generateGuestRoomLink: GenerateGuestRoomLinkUseCase + + @MockK + lateinit var validatePassword: ValidatePasswordUseCase + + @MockK + lateinit var generateRandomPasswordUseCase: GenerateRandomPasswordUseCase + + init { + MockKAnnotations.init(this) + every { + savedStateHandle.navArgs() + } returns CreatePasswordGuestLinkNavArgs( + conversationId = CONVERSATION_ID + ) + } + + fun withPasswordValidation(result: Boolean) = apply { + every { + validatePassword(any()) + } returns result + } + + fun withGenerateGuestLink( + result: GenerateGuestRoomLinkResult + ) = apply { + coEvery { + generateGuestRoomLink(any(), any()) + } returns result + } + + fun withGenerateRandomPassword( + result: String + ) = apply { + every { + generateRandomPasswordUseCase() + } returns result + } + + private val viewModel: CreatePasswordGuestLinkViewModel = CreatePasswordGuestLinkViewModel( + generateGuestRoomLink = generateGuestRoomLink, + validatePassword = validatePassword, + generateRandomPasswordUseCase = generateRandomPasswordUseCase, + savedStateHandle = savedStateHandle + ) + + fun arrange() = this to viewModel + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt index aac6988be86..b4c43fca4f5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt @@ -29,6 +29,7 @@ import com.wire.android.framework.TestConversation import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase import com.wire.android.ui.navArgs import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkResult @@ -161,13 +162,12 @@ class EditGuestAccessViewModelTest { @Test fun `given useCase runs with success, when_generating guest link, then invoke it once`() = runTest { coEvery { - generateGuestRoomLink(any()) + generateGuestRoomLink.invoke(any(), any()) } returns GenerateGuestRoomLinkResult.Success - editGuestAccessViewModel.onGenerateGuestRoomLink() - + editGuestAccessViewModel.onRequestGuestRoomLink() coVerify(exactly = 1) { - generateGuestRoomLink(any()) + generateGuestRoomLink.invoke(any(), null) } assertEquals(false, editGuestAccessViewModel.editGuestAccessState.isGeneratingGuestRoomLink) } @@ -175,13 +175,13 @@ class EditGuestAccessViewModelTest { @Test fun `given useCase runs with failure, when generating guest link, then show dialog error`() = runTest { coEvery { - generateGuestRoomLink(any()) - } returns GenerateGuestRoomLinkResult.Failure(CoreFailure.MissingClientRegistration) + generateGuestRoomLink(any(), any()) + } returns GenerateGuestRoomLinkResult.Failure(NetworkFailure.NoNetworkConnection(null)) - editGuestAccessViewModel.onGenerateGuestRoomLink() + editGuestAccessViewModel.onRequestGuestRoomLink() coVerify(exactly = 1) { - generateGuestRoomLink(any()) + generateGuestRoomLink(any(), null) } assertEquals(true, editGuestAccessViewModel.editGuestAccessState.isFailedToGenerateGuestRoomLink) } diff --git a/kalium b/kalium index 856da309995..23ccfbcfecc 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 856da30999548d19e08578515cbcd41075e25d85 +Subproject commit 23ccfbcfecca398ade7f43ce3446d8913454f7c6 From 36ff8db1f23173ae4f64a7dcf172ad9c6ef0e417 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 09:58:26 +0200 Subject: [PATCH 2/4] fix: markdown link crash (#2116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Żerko --- .../wire/android/LinkSpannableStringTest.kt | 103 ++++++++++++++++++ .../wire/android/ui/common/ClickableText.kt | 9 +- .../android/ui/markdown/MarkdownComposer.kt | 16 ++- 3 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 app/src/androidTest/java/com/wire/android/LinkSpannableStringTest.kt diff --git a/app/src/androidTest/java/com/wire/android/LinkSpannableStringTest.kt b/app/src/androidTest/java/com/wire/android/LinkSpannableStringTest.kt new file mode 100644 index 00000000000..4265250bf3a --- /dev/null +++ b/app/src/androidTest/java/com/wire/android/LinkSpannableStringTest.kt @@ -0,0 +1,103 @@ +/* + * 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 + +import android.text.style.URLSpan +import android.text.util.Linkify +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.wire.android.ui.common.LinkSpannableString +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LinkSpannableStringTest { + + lateinit var linkSpannableString: LinkSpannableString + + @Before + fun setUp() { + linkSpannableString = LinkSpannableString("Hello, world!") + } + + @Test + fun givenValidIndices_whenSetSpanIsCalled_thenSpanIsSet() { + // Given + val start = 0 + val end = 5 + + // When + linkSpannableString.setSpan(URLSpan("http://example.com"), start, end, 0) + + // Then + assert(linkSpannableString.getSpans(start, end, URLSpan::class.java).isNotEmpty()) + } + + @Test + fun givenInvalidStartIndex_whenSetSpanIsCalled_thenSpanIsNotSet() { + // Given + val start = -1 + val end = 5 + + // When + linkSpannableString.setSpan(URLSpan("http://example.com"), start, end, 0) + + // Then + assert(linkSpannableString.getSpans(start, end, URLSpan::class.java).isEmpty()) + } + + @Test + fun givenInvalidEndIndex_whenSetSpanIsCalled_thenSpanIsNotSet() { + // Given + val start = 0 + val end = 20 + + // When + linkSpannableString.setSpan(URLSpan("http://example.com"), start, end, 0) + + // Then + assert(linkSpannableString.getSpans(start, end, URLSpan::class.java).isEmpty()) + } + + @Test + fun givenASetSpan_whenRemoveSpanIsCalled_thenSpanIsRemoved() { + // Given + val urlSpan = URLSpan("http://example.com") + linkSpannableString.setSpan(urlSpan, 0, 5, 0) + + // When + linkSpannableString.removeSpan(urlSpan) + + // Then + assert(linkSpannableString.getSpans(0, 5, URLSpan::class.java).isEmpty()) + } + + @Test + fun givenATextWithLink_whenGetLinkInfosIsCalled_thenLinkInfoIsReturned() { + // Given + val text = "Visit http://example.com for more info." + val mask = Linkify.WEB_URLS + + // When + val linkInfos = LinkSpannableString.getLinkInfos(text, mask) + + // Then + assert(linkInfos.size == 1) + assert(linkInfos[0].url == "http://example.com") + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/ClickableText.kt b/app/src/main/kotlin/com/wire/android/ui/common/ClickableText.kt index 283b1d6f6e1..eb81491011a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/ClickableText.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/ClickableText.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow +import com.wire.android.appLogger @Composable fun ClickableText( @@ -137,7 +138,11 @@ class LinkSpannableString(source: CharSequence) : SpannableString(source) { } override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) { - super.setSpan(what, start, end, flags) - spanList.add(Data(what, start, end)) + if (start >= 0 && end <= this.length && start <= end) { + super.setSpan(what, start, end, flags) + spanList.add(Data(what, start, end)) + } else { + appLogger.e("[LinkSpannableString] Invalid span indices: start=$start, end=$end, length=$length") + } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt index 5f0a36d4cea..60ff8d40785 100644 --- a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt @@ -52,6 +52,8 @@ import org.commonmark.node.SoftLineBreak import org.commonmark.node.StrongEmphasis import org.commonmark.node.Text import org.commonmark.node.ThematicBreak +import kotlin.math.max +import kotlin.math.min import org.commonmark.node.Text as nodeText @Composable @@ -192,7 +194,7 @@ fun inlineChildren( return updatedMentions } -@Suppress("LongMethod") +@Suppress("LongMethod", "ComplexMethod") fun appendLinksAndMentions( annotatedString: AnnotatedString.Builder, string: String, @@ -233,7 +235,9 @@ fun appendLinksAndMentions( append(stringBuilder) with(nodeData.colorScheme) { linkInfos.forEach { - if (it.end - it.start <= 0) { + val safeStart = max(it.start, 0) + val safeEnd = min(it.end, length - 1) + if (safeStart > safeEnd) { return@forEach } addStyle( @@ -241,14 +245,14 @@ fun appendLinksAndMentions( color = primary, textDecoration = TextDecoration.Underline ), - start = it.start, - end = it.end + start = safeStart, + end = safeEnd ) addStringAnnotation( tag = TAG_URL, annotation = it.url, - start = it.start, - end = it.end + start = safeStart, + end = safeEnd ) } From 65457e5c11f99d7d6ac7b8730d5394b7514d3494 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 09:20:04 +0000 Subject: [PATCH 3/4] fix: spannable link in markdown (#2119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Żerko --- .../ui/home/conversations/mock/Mock.kt | 146 ++++++++++++++++++ .../model/MessageTypesPreview.kt | 66 ++++++++ .../android/ui/markdown/MarkdownComposer.kt | 2 +- 3 files changed, 213 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt index 9c50a96780b..5fb59824cf0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt @@ -90,6 +90,152 @@ val mockMessageWithText = UIMessage.Regular( messageFooter = mockEmptyFooter ) +val mockMessageWithMarkdownTextAndLinks = UIMessage.Regular( + userAvatarData = UserAvatarData(null, UserAvailabilityStatus.AVAILABLE), + header = mockHeader, + messageContent = UIMessageContent.TextMessage( + messageBody = MessageBody( + UIText.DynamicString( + """ +**bold text** + +_italic text_ + +**_bold and italic_** + +~~Strikethrough~~ + +# header + +# Code + +Inline `code` + +Indented code + +// Some comments +line 1 of code +line 2 of code +line 3 of code + + +Block code "fences" + +``` +Sample text here... +``` + +# Links +[AR PR](https://github.com/wireapp/wire-android-reloaded/pulls) + +Autoconverted link https://github.com/wireapp/kalium/pulls +""" + ) + ) + ), + source = MessageSource.Self, + messageFooter = mockEmptyFooter +) + +val mockMessageWithMarkdownListAndImages = UIMessage.Regular( + userAvatarData = UserAvatarData(null, UserAvailabilityStatus.AVAILABLE), + header = mockHeader, + messageContent = UIMessageContent.TextMessage( + messageBody = MessageBody( + UIText.DynamicString( + """ +## Lists + +Bullet List + ++ Create a list by starting a line with `+`, `-`, or `*` ++ Sub-lists are made by indenting 2 spaces: +- Marker character change forces new list start: +* Ac tristique libero volutpat at ++ Facilisis in pretium nisl aliquet +- Nulla volutpat aliquam velit ++ Very easy! + +Ordered + +1. Lorem ipsum dolor sit amet +2. Consectetur adipiscing elit +3. Integer molestie lorem at massa + + +1. You can use sequential numbers... +1. ...or keep all the numbers as `1.` + +Start numbering with offset: + +57. foo +1. bar + +# Images + +Webp + +![Wire](https://wire.com/wp-content/uploads/2022/02/Independently-Audited-_-Open-Source-2.webp) + +Svg + +![Wire](https://wire.com/wp-content/uploads/2021/08/wire-logo.svg) + +Png + +![Wire](https://avatars.githubusercontent.com/u/16047324?s=280&v=4) +""" + ) + ) + ), + source = MessageSource.Self, + messageFooter = mockEmptyFooter +) + +val mockMessageWithMarkdownTablesAndBlocks = UIMessage.Regular( + userAvatarData = UserAvatarData(null, UserAvailabilityStatus.AVAILABLE), + header = mockHeader, + messageContent = UIMessageContent.TextMessage( + messageBody = MessageBody( + UIText.DynamicString( + """ +# Tables +| Task | Person | +| ------ | ----------- | +| MLS | John | +| Federation | Will | +| Navigation | Ashley | + + +## Thematic Break + +___ + +--- + +*** + + +# Blockquotes + + +> Blockquotes can also be nested... +>> ...by using additional greater-than signs right next to each other... +> > > ...or with spaces between arrows. + + +# Typographic replacements + +Enable typographer option to see result. + +(c) (C) (r) (R) (tm) (TM) (p) (P) +-""" + ) + ) + ), + source = MessageSource.Self, + messageFooter = mockEmptyFooter +) + val mockMessageWithKnock = UIMessage.System( header = mockHeader, messageContent = UIMessageContent.SystemMessage.Knock(UIText.DynamicString("John Doe pinged")), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypesPreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypesPreview.kt index 1adc4cc8f4c..3c1e6bc5e00 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypesPreview.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypesPreview.kt @@ -33,6 +33,9 @@ import com.wire.android.ui.home.conversations.mock.mockAssetMessage import com.wire.android.ui.home.conversations.mock.mockFooter import com.wire.android.ui.home.conversations.mock.mockHeader import com.wire.android.ui.home.conversations.mock.mockMessageWithKnock +import com.wire.android.ui.home.conversations.mock.mockMessageWithMarkdownTextAndLinks +import com.wire.android.ui.home.conversations.mock.mockMessageWithMarkdownListAndImages +import com.wire.android.ui.home.conversations.mock.mockMessageWithMarkdownTablesAndBlocks import com.wire.android.ui.home.conversations.mock.mockMessageWithText import com.wire.android.ui.home.conversations.mock.mockedImageUIMessage import com.wire.android.ui.theme.WireTheme @@ -522,3 +525,66 @@ fun PreviewAggregatedMessagesWithErrorMessage() { } } } + +@PreviewMultipleThemes +@Composable +fun PreviewMessageWithMarkdownTextAndLinks() { + WireTheme { + MessageItem( + message = mockMessageWithMarkdownTextAndLinks, + audioMessagesState = emptyMap(), + onLongClicked = {}, + onAssetMessageClicked = {}, + onAudioClick = {}, + onChangeAudioPosition = { _, _ -> }, + onImageMessageClicked = { _, _ -> }, + onOpenProfile = { _ -> }, + onReactionClicked = { _, _ -> }, + onResetSessionClicked = { _, _ -> }, + onSelfDeletingMessageRead = {}, + conversationDetailsData = ConversationDetailsData.None + ) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewMessageWithMarkdownListAndImages() { + WireTheme { + MessageItem( + message = mockMessageWithMarkdownListAndImages, + audioMessagesState = emptyMap(), + onLongClicked = {}, + onAssetMessageClicked = {}, + onAudioClick = {}, + onChangeAudioPosition = { _, _ -> }, + onImageMessageClicked = { _, _ -> }, + onOpenProfile = { _ -> }, + onReactionClicked = { _, _ -> }, + onResetSessionClicked = { _, _ -> }, + onSelfDeletingMessageRead = {}, + conversationDetailsData = ConversationDetailsData.None + ) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewMessageWithMarkdownTablesAndBlocks() { + WireTheme { + MessageItem( + message = mockMessageWithMarkdownTablesAndBlocks, + audioMessagesState = emptyMap(), + onLongClicked = {}, + onAssetMessageClicked = {}, + onAudioClick = {}, + onChangeAudioPosition = { _, _ -> }, + onImageMessageClicked = { _, _ -> }, + onOpenProfile = { _ -> }, + onReactionClicked = { _, _ -> }, + onResetSessionClicked = { _, _ -> }, + onSelfDeletingMessageRead = {}, + conversationDetailsData = ConversationDetailsData.None + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt index 60ff8d40785..99abdd3f29c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt @@ -236,7 +236,7 @@ fun appendLinksAndMentions( with(nodeData.colorScheme) { linkInfos.forEach { val safeStart = max(it.start, 0) - val safeEnd = min(it.end, length - 1) + val safeEnd = min(it.end, length) if (safeStart > safeEnd) { return@forEach } From e8638e5797ebd1f1459bab21e43f19a5a3c39266 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 10:47:32 +0200 Subject: [PATCH 4/4] fix: add call to action settings icons (WPB-3065) (#2127) Co-authored-by: yamilmedina --- .../wire/android/ui/debug/DebugDataOptions.kt | 85 +++++++++++++++---- .../com/wire/android/ui/debug/LogOptions.kt | 15 +++- .../android/ui/home/settings/SettingsItem.kt | 6 +- .../ui/home/settings/SettingsScreen.kt | 4 +- app/src/main/res/values/strings.xml | 2 +- 5 files changed, 88 insertions(+), 24 deletions(-) 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 ba35d1e1065..7d5fb62ea0f 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 @@ -20,6 +20,7 @@ package com.wire.android.ui.debug import android.content.Context import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -49,11 +50,12 @@ import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.getDeviceId 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.keypackage.MLSKeyPackageCountResult import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase -import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler +import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.first @@ -169,14 +171,36 @@ class DebugDataOptionsViewModel @Composable fun DebugDataOptions( + viewModel: DebugDataOptionsViewModel = hiltViewModel(), appVersion: String, buildVariant: String, onCopyText: (String) -> Unit, onManualMigrationPressed: (currentAccount: UserId) -> Unit ) { + DebugDataOptionsContent( + state = viewModel.state, + appVersion = appVersion, + buildVariant = buildVariant, + onCopyText = onCopyText, + onEnableEncryptedProteusStorageChange = viewModel::enableEncryptedProteusStorage, + onRestartSlowSyncForRecovery = viewModel::restartSlowSyncForRecovery, + onForceUpdateApiVersions = viewModel::forceUpdateApiVersions, + onManualMigrationPressed = { onManualMigrationPressed(viewModel.currentAccount) } + ) +} - val viewModel: DebugDataOptionsViewModel = hiltViewModel() - +@Suppress("LongParameterList") +@Composable +fun DebugDataOptionsContent( + state: DebugDataOptionsState, + appVersion: String, + buildVariant: String, + onCopyText: (String) -> Unit, + onEnableEncryptedProteusStorageChange: (Boolean) -> Unit, + onRestartSlowSyncForRecovery: () -> Unit, + onForceUpdateApiVersions: () -> Unit, + onManualMigrationPressed: () -> Unit +) { Column { FolderHeader(stringResource(R.string.label_debug_data)) @@ -203,11 +227,11 @@ fun DebugDataOptions( SettingsItem( title = stringResource(R.string.label_code_commit_id), - text = viewModel.state.commitish, + text = state.commitish, trailingIcon = R.drawable.ic_copy, onIconPressed = Clickable( enabled = true, - onClick = { onCopyText(viewModel.state.commitish) } + onClick = { onCopyText(state.commitish) } ) ) @@ -215,34 +239,34 @@ fun DebugDataOptions( SettingsItem( title = stringResource(R.string.debug_id), - text = viewModel.state.debugId, + text = state.debugId, trailingIcon = R.drawable.ic_copy, onIconPressed = Clickable( enabled = true, - onClick = { onCopyText(viewModel.state.debugId) } + onClick = { onCopyText(state.debugId) } ) ) ProteusOptions( - isEncryptedStorageEnabled = viewModel.state.isEncryptedProteusStorageEnabled, - onEncryptedStorageEnabledChange = viewModel::enableEncryptedProteusStorage + isEncryptedStorageEnabled = state.isEncryptedProteusStorageEnabled, + onEncryptedStorageEnabledChange = onEnableEncryptedProteusStorageChange ) MLSOptions( - keyPackagesCount = viewModel.state.keyPackagesCount, - mlsClientId = viewModel.state.mslClientId, - mlsErrorMessage = viewModel.state.mlsErrorMessage, - restartSlowSyncForRecovery = viewModel::restartSlowSyncForRecovery, + keyPackagesCount = state.keyPackagesCount, + mlsClientId = state.mslClientId, + mlsErrorMessage = state.mlsErrorMessage, + restartSlowSyncForRecovery = onRestartSlowSyncForRecovery, onCopyText = onCopyText ) - DevelopmentApiVersioningOptions(onForceLatestDevelopmentApiChange = viewModel::forceUpdateApiVersions) + DevelopmentApiVersioningOptions(onForceLatestDevelopmentApiChange = onForceUpdateApiVersions) } - FolderHeader("Other Debug Options") - if (viewModel.state.isManualMigrationAllowed) { + if (state.isManualMigrationAllowed) { + FolderHeader("Other Debug Options") ManualMigrationOptions( - onManualMigrationClicked = { onManualMigrationPressed(viewModel.currentAccount) } + onManualMigrationClicked = onManualMigrationPressed ) } } @@ -399,9 +423,34 @@ private fun EnableEncryptedProteusStorageSwitch( checked = isEnabled, onCheckedChange = onCheckedChange, enabled = !isEnabled, - modifier = Modifier.padding(end = dimensions().spacing16x) + modifier = Modifier + .padding(end = dimensions().spacing8x) + .size(dimensions().buttonSmallMinSize) ) } ) } //endregion + +@PreviewMultipleThemes +@Composable +fun PreviewOtherDebugOptions() { + DebugDataOptionsContent( + appVersion = "1.0.0", + buildVariant = "debug", + onCopyText = {}, + state = DebugDataOptionsState( + isEncryptedProteusStorageEnabled = true, + keyPackagesCount = 10, + mslClientId = "clientId", + mlsErrorMessage = "error", + isManualMigrationAllowed = true, + debugId = "debugId", + commitish = "commitish" + ), + onEnableEncryptedProteusStorageChange = {}, + onForceUpdateApiVersions = {}, + onRestartSlowSyncForRecovery = {}, + onManualMigrationPressed = {} + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/LogOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/LogOptions.kt index 7f0205a50d7..5a1af591554 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/LogOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/LogOptions.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -42,6 +43,7 @@ import com.wire.android.ui.home.settings.SettingsItem 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 @Composable fun LogOptions( @@ -107,7 +109,7 @@ private fun EnableLoggingSwitch( WireSwitch( checked = isEnabled, onCheckedChange = onCheckedChange, - modifier = Modifier.padding(end = dimensions().spacing16x) + modifier = Modifier.size(dimensions().buttonSmallMinSize) ) } } @@ -119,3 +121,14 @@ private fun EnableLoggingSwitch( } } } + +@PreviewMultipleThemes +@Composable +fun PreviewLoggingOptions() { + LogOptions( + isLoggingEnabled = true, + onLoggingEnabledChange = {}, + onDeleteLogs = {}, + onShareLogs = {} + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsItem.kt index 06a7f530c31..46abf62363c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsItem.kt @@ -31,7 +31,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.spec.Direction import com.wire.android.R import com.wire.android.model.Clickable @@ -85,7 +84,8 @@ fun SettingsItem( contentDescription = "", tint = MaterialTheme.wireColorScheme.onSecondaryButtonEnabled, modifier = Modifier - .defaultMinSize(80.dp) + .defaultMinSize(dimensions().wireIconButtonSize) + .padding(end = dimensions().spacing8x) .clickable(onIconPressed) ) } ?: Icons.Filled.ChevronRight @@ -157,7 +157,7 @@ fun previewFileRestrictionDialog() { SettingsItem( title = "Some Setting", text = "This is the value of the setting", - trailingIcon = R.drawable.ic_copy + trailingIcon = R.drawable.ic_arrow_right ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt index 3f565418392..4e54ee14948 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt @@ -127,7 +127,9 @@ private fun LazyListScope.folderWithElements( ) { settingsItem -> SettingsItem( text = settingsItem.title.asString(), - onRowPressed = remember { Clickable(enabled = true) { onItemClicked(settingsItem) } } + onRowPressed = remember { Clickable(enabled = true) { onItemClicked(settingsItem) } }, + onIconPressed = remember { Clickable(enabled = true) { onItemClicked(settingsItem) } }, + trailingIcon = R.drawable.ic_arrow_right, ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 88f32f2ee34..ffe2b17a33d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -897,7 +897,7 @@ Debug Settings Client ID Device ID: %1$s - Commit Hash: %1$s + Commit Hash Client ID: %1$s ADDED LAST ACTIVE