From 714395226019d67a053a2d2af0653aefd3c59d92 Mon Sep 17 00:00:00 2001 From: David Perez Date: Tue, 31 Mar 2026 11:37:33 -0500 Subject: [PATCH] Create common UI elements for VaultItemScreen --- .../feature/item/VaultItemCardContent.kt | 175 ++------------- .../feature/item/VaultItemIdentityContent.kt | 165 ++------------- .../feature/item/VaultItemLoginContent.kt | 199 ++---------------- .../item/VaultItemSecureNoteContent.kt | 179 ++-------------- .../feature/item/VaultItemSshKeyContent.kt | 174 ++------------- .../vault/feature/item/VaultItemViewModel.kt | 24 ++- .../VaultItemAttachment.kt} | 5 +- .../item/component/VaultItemAttachments.kt | 61 ++++++ .../item/component/VaultItemCustomFields.kt | 59 ++++++ .../item/component/VaultItemHistory.kt | 97 +++++++++ .../feature/item/component/VaultItemNotes.kt | 69 ++++++ .../feature/item/util/CipherViewExtensions.kt | 23 +- .../vault/feature/item/VaultItemScreenTest.kt | 44 ++-- .../feature/item/VaultItemViewModelTest.kt | 8 +- .../feature/item/util/VaultItemTestUtil.kt | 8 +- 15 files changed, 454 insertions(+), 836 deletions(-) rename app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/{VaultItemAttachmentContent.kt => component/VaultItemAttachment.kt} (97%) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemAttachments.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemCustomFields.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemHistory.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemNotes.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index d22c6c91e32..b7a82245725 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -23,14 +19,13 @@ import com.bitwarden.ui.platform.base.util.toListItemCardStyle import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.bitwarden.ui.platform.components.field.BitwardenTextField -import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText -import com.bitwarden.ui.platform.components.model.CardStyle -import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString -import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemAttachments +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemCustomFields +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemHistory +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemNotes import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand @@ -207,154 +202,26 @@ fun VaultItemCardContent( } } - commonState.notes?.let { notes -> - item(key = "notes") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = BitwardenString.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = BitwardenDrawable.ic_copy, - contentDescription = stringResource(id = BitwardenString.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item(key = "customFieldsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - } - itemsIndexed( - items = customFields, - key = { index, _ -> "customField_$index" }, - ) { _, customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item(key = "attachmentsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed( - items = attachments, - key = { index, _ -> "attachment_$index" }, - ) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - onAttachmentPreviewClick = vaultCommonItemTypeHandlers.onAttachmentPreviewClick, - onUpgradeToPremiumClick = vaultCommonItemTypeHandlers.onUpgradeToPremiumClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } + vaultItemNotes( + notes = commonState.notes, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "lastUpdated") { - Spacer(modifier = Modifier.height(height = 16.dp)) - Text( - text = commonState.lastUpdated(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("CardItemLastUpdated"), - ) - } + vaultItemCustomFields( + customFields = commonState.customFields, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "created") { - Spacer(modifier = Modifier.height(height = 4.dp)) - Text( - text = commonState.created(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("CardItemCreated"), - ) - } + vaultItemAttachments( + attachments = commonState.attachments, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - commonState.passwordHistoryCount?.let { passwordHistoryCount -> - item(key = "passwordHistoryCount") { - Spacer(modifier = Modifier.height(height = 4.dp)) - BitwardenHyperTextLink( - annotatedResId = BitwardenString.password_history_count, - args = arrayOf(passwordHistoryCount.toString()), - annotationKey = "passwordHistory", - accessibilityString = stringResource(id = BitwardenString.password_history), - onClick = vaultCommonItemTypeHandlers.onPasswordHistoryClick, - style = BitwardenTheme.typography.labelMedium, - modifier = Modifier - .wrapContentWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem(), - ) - } - } + vaultItemHistory( + commonState = commonState, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + loginPasswordRevisionDate = null, + ) item { Spacer(modifier = Modifier.height(88.dp)) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt index a72bad6a315..fac104ef79e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt @@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -23,15 +19,15 @@ import com.bitwarden.ui.platform.base.util.toListItemCardStyle import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.bitwarden.ui.platform.components.field.BitwardenTextField -import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.model.CardStyle -import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString -import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemAttachments +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemCustomFields +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemHistory +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemNotes import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers @@ -275,146 +271,27 @@ fun VaultItemIdentityContent( ) } } - commonState.notes?.let { notes -> - item(key = "notes") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - Spacer(modifier = Modifier.height(8.dp)) - IdentityCopyField( - label = stringResource(id = BitwardenString.notes), - value = notes, - copyContentDescription = stringResource(id = BitwardenString.copy_notes), - textFieldTestTag = "CipherNotesLabel", - copyActionTestTag = "CipherNotesCopyButton", - onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item(key = "customFieldsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - } - itemsIndexed( - items = customFields, - key = { index, _ -> "customField_$index" }, - ) { _, customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - ) - } - } - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item(key = "attachmentsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed( - items = attachments, - key = { index, _ -> "attachment_$index" }, - ) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - onAttachmentPreviewClick = vaultCommonItemTypeHandlers.onAttachmentPreviewClick, - onUpgradeToPremiumClick = vaultCommonItemTypeHandlers.onUpgradeToPremiumClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } + vaultItemNotes( + notes = commonState.notes, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "lastUpdated") { - Spacer(modifier = Modifier.height(height = 16.dp)) - Text( - text = commonState.lastUpdated(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("IdentityItemLastUpdated"), - ) - } + vaultItemCustomFields( + customFields = commonState.customFields, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "created") { - Spacer(modifier = Modifier.height(height = 4.dp)) - Text( - text = commonState.created(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("IdentityItemCreated"), - ) - } + vaultItemAttachments( + attachments = commonState.attachments, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - commonState.passwordHistoryCount?.let { passwordHistoryCount -> - item(key = "passwordHistoryCount") { - Spacer(modifier = Modifier.height(height = 4.dp)) - BitwardenHyperTextLink( - annotatedResId = BitwardenString.password_history_count, - args = arrayOf(passwordHistoryCount.toString()), - annotationKey = "passwordHistory", - accessibilityString = stringResource(id = BitwardenString.password_history), - onClick = vaultCommonItemTypeHandlers.onPasswordHistoryClick, - style = BitwardenTheme.typography.labelMedium, - modifier = Modifier - .wrapContentWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem(), - ) - } - } + vaultItemHistory( + commonState = commonState, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + loginPasswordRevisionDate = null, + ) item { Spacer(modifier = Modifier.height(88.dp)) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 19060de73f2..dbe8ba0b773 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -6,10 +6,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -31,12 +29,14 @@ import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.components.text.BitwardenClickableText -import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemAttachments +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemCustomFields +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemHistory +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemNotes import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData @@ -46,7 +46,7 @@ private const val AUTH_CODE_SPACING_INTERVAL = 3 /** * The top level content UI state for the [VaultItemScreen] when viewing a Login cipher. */ -@Suppress("LongMethod", "CyclomaticComplexMethod") +@Suppress("LongMethod") @Composable fun VaultItemLoginContent( commonState: VaultItemState.ViewState.Content.Common, @@ -186,155 +186,26 @@ fun VaultItemLoginContent( } } - commonState.notes?.let { notes -> - item(key = "notes") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.additional_options), - modifier = Modifier - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .fillMaxWidth() - .animateItem(), - ) - Spacer(modifier = Modifier.height(8.dp)) - NotesField( - notes = notes, - onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier - .standardHorizontalMargin() - .fillMaxWidth() - .animateItem(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item(key = "customFieldsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - } - itemsIndexed( - items = customFields, - key = { index, _ -> "customField_$index" }, - ) { _, customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .standardHorizontalMargin() - .fillMaxWidth() - .animateItem(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item(key = "attachmentsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.attachments), - modifier = Modifier - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .fillMaxWidth() - .animateItem(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed( - items = attachments, - key = { index, _ -> "attachment_$index" }, - ) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .standardHorizontalMargin() - .fillMaxWidth() - .animateItem(), - attachmentItem = attachmentItem, - cardStyle = attachments.toListItemCardStyle(index = index), - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - onAttachmentPreviewClick = vaultCommonItemTypeHandlers.onAttachmentPreviewClick, - onUpgradeToPremiumClick = vaultCommonItemTypeHandlers.onUpgradeToPremiumClick, - ) - } - } + vaultItemNotes( + notes = commonState.notes, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "lastUpdated") { - Spacer(modifier = Modifier.height(height = 16.dp)) - Text( - text = commonState.lastUpdated(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("LoginItemLastUpdated"), - ) - } + vaultItemCustomFields( + customFields = commonState.customFields, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "created") { - Spacer(modifier = Modifier.height(height = 4.dp)) - Text( - text = commonState.created(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("LoginItemCreated"), - ) - } + vaultItemAttachments( + attachments = commonState.attachments, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - loginItemState.passwordRevisionDate?.let { revisionDate -> - item(key = "revisionDate") { - Spacer(modifier = Modifier.height(height = 4.dp)) - Text( - text = revisionDate(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem(), - ) - } - } - commonState.passwordHistoryCount?.let { passwordHistoryCount -> - item(key = "passwordHistoryCount") { - Spacer(modifier = Modifier.height(height = 4.dp)) - BitwardenHyperTextLink( - annotatedResId = BitwardenString.password_history_count, - args = arrayOf(passwordHistoryCount.toString()), - annotationKey = "passwordHistory", - accessibilityString = stringResource(id = BitwardenString.password_history), - onClick = vaultCommonItemTypeHandlers.onPasswordHistoryClick, - style = BitwardenTheme.typography.labelMedium, - modifier = Modifier - .wrapContentWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem(), - ) - } - } + vaultItemHistory( + commonState = commonState, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + loginPasswordRevisionDate = loginItemState.passwordRevisionDate, + ) item { Spacer(modifier = Modifier.height(88.dp)) @@ -359,32 +230,6 @@ private fun Fido2CredentialField( ) } -@Composable -private fun NotesField( - notes: String, - onCopyAction: () -> Unit, - modifier: Modifier = Modifier, -) { - BitwardenTextField( - label = stringResource(id = BitwardenString.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = BitwardenDrawable.ic_copy, - contentDescription = stringResource(id = BitwardenString.copy_notes), - onClick = onCopyAction, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = modifier, - ) -} - @Composable private fun PasswordField( passwordData: VaultItemState.ViewState.Content.ItemType.Login.PasswordData, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index 362b7ca5f50..9d72e135161 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -4,39 +4,25 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.bitwarden.ui.platform.base.util.standardHorizontalMargin -import com.bitwarden.ui.platform.base.util.toListItemCardStyle -import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton -import com.bitwarden.ui.platform.components.field.BitwardenTextField -import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.bitwarden.ui.platform.components.icon.model.IconData -import com.bitwarden.ui.platform.components.model.CardStyle -import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink -import com.bitwarden.ui.platform.resource.BitwardenDrawable -import com.bitwarden.ui.platform.resource.BitwardenString -import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemAttachments +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemCustomFields +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemHistory +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemNotes import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers /** * The top level content UI state for the [VaultItemScreen] when viewing a secure note cipher. */ -@Suppress("LongMethod") @Composable fun VaultItemSecureNoteContent( commonState: VaultItemState.ViewState.Content.Common, @@ -60,149 +46,28 @@ fun VaultItemSecureNoteContent( onExpandClick = { isExpanded = !isExpanded }, applyIconBackground = commonState.iconData is IconData.Local, ) - item { - Spacer(modifier = Modifier.height(height = 8.dp)) - } - commonState.notes?.let { notes -> - item(key = "notes") { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = BitwardenString.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = BitwardenDrawable.ic_copy, - contentDescription = stringResource(id = BitwardenString.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item(key = "customFieldsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - } - itemsIndexed( - items = customFields, - key = { index, _ -> "customField_$index" }, - ) { _, customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item(key = "attachmentsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed( - items = attachments, - key = { index, _ -> "attachment_$index" }, - ) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .testTag("CipherAttachment") - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - onAttachmentPreviewClick = vaultCommonItemTypeHandlers.onAttachmentPreviewClick, - onUpgradeToPremiumClick = vaultCommonItemTypeHandlers.onUpgradeToPremiumClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } + vaultItemNotes( + notes = commonState.notes, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + showHeader = false, + ) - item(key = "lastUpdated") { - Spacer(modifier = Modifier.height(height = 16.dp)) - Text( - text = commonState.lastUpdated(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("SecureNoteItemLastUpdated"), - ) - } + vaultItemCustomFields( + customFields = commonState.customFields, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "created") { - Spacer(modifier = Modifier.height(height = 4.dp)) - Text( - text = commonState.created(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("SecureNoteItemCreated"), - ) - } + vaultItemAttachments( + attachments = commonState.attachments, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - commonState.passwordHistoryCount?.let { passwordHistoryCount -> - item(key = "passwordHistoryCount") { - Spacer(modifier = Modifier.height(height = 4.dp)) - BitwardenHyperTextLink( - annotatedResId = BitwardenString.password_history_count, - args = arrayOf(passwordHistoryCount.toString()), - annotationKey = "passwordHistory", - accessibilityString = stringResource(id = BitwardenString.password_history), - onClick = vaultCommonItemTypeHandlers.onPasswordHistoryClick, - style = BitwardenTheme.typography.labelMedium, - modifier = Modifier - .wrapContentWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem(), - ) - } - } + vaultItemHistory( + commonState = commonState, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + loginPasswordRevisionDate = null, + ) item { Spacer(modifier = Modifier.height(88.dp)) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt index 910bed29319..c076e816bcd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt @@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -19,19 +15,18 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.base.util.standardHorizontalMargin -import com.bitwarden.ui.platform.base.util.toListItemCardStyle import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.bitwarden.ui.platform.components.field.BitwardenTextField -import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.model.CardStyle -import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString -import com.bitwarden.ui.platform.theme.BitwardenTheme -import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemAttachments +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemCustomFields +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemHistory +import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemNotes import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers @@ -141,153 +136,26 @@ fun VaultItemSshKeyContent( ) } - commonState.notes?.let { notes -> - item(key = "notes") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.additional_options), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = BitwardenString.notes), - value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, - actions = { - BitwardenStandardIconButton( - vectorIconRes = BitwardenDrawable.ic_copy, - contentDescription = stringResource(id = BitwardenString.copy_notes), - onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, - modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), - ) - }, - textFieldTestTag = "CipherNotesLabel", - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - ) - } - } - - commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> - item(key = "customFieldsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.custom_fields), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - } - itemsIndexed( - items = customFields, - key = { index, _ -> "customField_$index" }, - ) { _, customField -> - Spacer(modifier = Modifier.height(height = 8.dp)) - CustomField( - customField = customField, - onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, - onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, - onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, - cardStyle = CardStyle.Full, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - ) - } - } - - commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> - item(key = "attachmentsHeader") { - Spacer(modifier = Modifier.height(height = 16.dp)) - BitwardenListHeaderText( - label = stringResource(id = BitwardenString.attachments), - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 16.dp) - .animateItem(), - ) - Spacer(modifier = Modifier.height(height = 8.dp)) - } - itemsIndexed( - items = attachments, - key = { index, _ -> "attachment_$index" }, - ) { index, attachmentItem -> - AttachmentItemContent( - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .animateItem(), - attachmentItem = attachmentItem, - onAttachmentDownloadClick = vaultCommonItemTypeHandlers - .onAttachmentDownloadClick, - onAttachmentPreviewClick = vaultCommonItemTypeHandlers.onAttachmentPreviewClick, - onUpgradeToPremiumClick = vaultCommonItemTypeHandlers.onUpgradeToPremiumClick, - cardStyle = attachments.toListItemCardStyle(index = index), - ) - } - } + vaultItemNotes( + notes = commonState.notes, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "lastUpdated") { - Spacer(modifier = Modifier.height(height = 16.dp)) - Text( - text = commonState.lastUpdated(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("SshKeyItemLastUpdated"), - ) - } + vaultItemCustomFields( + customFields = commonState.customFields, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - item(key = "created") { - Spacer(modifier = Modifier.height(height = 4.dp)) - Text( - text = commonState.created(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem() - .testTag("SshKeyItemCreated"), - ) - } + vaultItemAttachments( + attachments = commonState.attachments, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + ) - commonState.passwordHistoryCount?.let { passwordHistoryCount -> - item(key = "passwordHistoryCount") { - Spacer(modifier = Modifier.height(height = 4.dp)) - BitwardenHyperTextLink( - annotatedResId = BitwardenString.password_history_count, - args = arrayOf(passwordHistoryCount.toString()), - annotationKey = "passwordHistory", - accessibilityString = stringResource(id = BitwardenString.password_history), - onClick = vaultCommonItemTypeHandlers.onPasswordHistoryClick, - style = BitwardenTheme.typography.labelMedium, - modifier = Modifier - .wrapContentWidth() - .standardHorizontalMargin() - .padding(horizontal = 12.dp) - .animateItem(), - ) - } - } + vaultItemHistory( + commonState = commonState, + vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + loginPasswordRevisionDate = null, + ) item { Spacer(modifier = Modifier.height(88.dp)) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 131a997298a..02566613174 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -365,13 +365,17 @@ class VaultItemViewModel @Inject constructor( currentState.copy( viewState = content.copy( common = content.common.copy( - customFields = content.common.customFields.map { customField -> - if (customField == action.field) { - action.field.copy(isVisible = action.isVisible) - } else { - customField + customFields = content + .common + .customFields + .map { customField -> + if (customField == action.field) { + action.field.copy(isVisible = action.isVisible) + } else { + customField + } } - }, + .toImmutableList(), ), ), ) @@ -1598,11 +1602,11 @@ data class VaultItemState( val created: Text, val lastUpdated: Text, val notes: String?, - val customFields: List, + val customFields: ImmutableList, val requiresCloneConfirmation: Boolean, @IgnoredOnParcel val currentCipher: CipherView? = null, - val attachments: List?, + val attachments: ImmutableList, val canDelete: Boolean, val canRestore: Boolean, val canAssignToCollections: Boolean, @@ -1792,7 +1796,7 @@ data class VaultItemState( /** * An ordered list of Card specific elements. */ - val propertyList: List + val propertyList: ImmutableList get() = persistentListOfNotNull( identityName, username, @@ -1828,7 +1832,7 @@ data class VaultItemState( /** * An ordered list of Card specific elements. */ - val propertyList: List + val propertyList: ImmutableList get() = persistentListOfNotNull( cardholderName, number, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemAttachmentContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemAttachment.kt similarity index 97% rename from app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemAttachmentContent.kt rename to app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemAttachment.kt index 9a1a2038a1d..6d4dfa062f4 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemAttachmentContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemAttachment.kt @@ -1,4 +1,4 @@ -package com.x8bit.bitwarden.ui.vault.feature.item +package com.x8bit.bitwarden.ui.vault.feature.item.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -26,13 +26,14 @@ import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme +import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState /** * Attachment UI common for all item types. */ @Suppress("LongMethod") @Composable -fun AttachmentItemContent( +fun VaultItemAttachment( attachmentItem: VaultItemState.ViewState.Content.Common.AttachmentItem, onAttachmentDownloadClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit, onAttachmentPreviewClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemAttachments.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemAttachments.kt new file mode 100644 index 00000000000..9a6bd0c3a1e --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemAttachments.kt @@ -0,0 +1,61 @@ +package com.x8bit.bitwarden.ui.vault.feature.item.component + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.bitwarden.ui.platform.base.util.toListItemCardStyle +import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.bitwarden.ui.platform.resource.BitwardenString +import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState +import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers +import kotlinx.collections.immutable.ImmutableList + +/** + * Displays the common attachment items for the vault item screen. + * + * @param attachments The attachments to display. + * @param vaultCommonItemTypeHandlers Provides the handlers required for each attachment. + */ +fun LazyListScope.vaultItemAttachments( + attachments: ImmutableList, + vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, +) { + if (attachments.isEmpty()) return + item(key = "attachmentsHeader") { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = BitwardenString.attachments), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp) + .animateItem(), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + itemsIndexed( + items = attachments, + key = { index, _ -> "attachment_$index" }, + ) { index, attachmentItem -> + VaultItemAttachment( + attachmentItem = attachmentItem, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers.onAttachmentDownloadClick, + onAttachmentPreviewClick = vaultCommonItemTypeHandlers.onAttachmentPreviewClick, + onUpgradeToPremiumClick = vaultCommonItemTypeHandlers.onUpgradeToPremiumClick, + cardStyle = attachments.toListItemCardStyle(index = index), + modifier = Modifier + .testTag(tag = "CipherAttachment") + .fillMaxWidth() + .standardHorizontalMargin() + .animateItem(), + ) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemCustomFields.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemCustomFields.kt new file mode 100644 index 00000000000..dba30dcde47 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemCustomFields.kt @@ -0,0 +1,59 @@ +package com.x8bit.bitwarden.ui.vault.feature.item.component + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.bitwarden.ui.platform.components.model.CardStyle +import com.bitwarden.ui.platform.resource.BitwardenString +import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState.ViewState.Content.Common.Custom +import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers +import kotlinx.collections.immutable.ImmutableList + +/** + * Displays the common custom field items for the vault item screen. + * + * @param customFields The custom fields to display. + * @param vaultCommonItemTypeHandlers Provides the handlers required for each custom field. + */ +fun LazyListScope.vaultItemCustomFields( + customFields: ImmutableList, + vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, +) { + if (customFields.isEmpty()) return + item(key = "customFieldsHeader") { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = BitwardenString.custom_fields), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp) + .animateItem(), + ) + } + itemsIndexed( + items = customFields, + key = { index, _ -> "customField_$index" }, + ) { _, customField -> + Spacer(modifier = Modifier.height(height = 8.dp)) + CustomField( + customField = customField, + onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField, + onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField, + onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .animateItem(), + ) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemHistory.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemHistory.kt new file mode 100644 index 00000000000..0e771625b7e --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemHistory.kt @@ -0,0 +1,97 @@ +package com.x8bit.bitwarden.ui.vault.feature.item.component + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink +import com.bitwarden.ui.platform.resource.BitwardenString +import com.bitwarden.ui.platform.theme.BitwardenTheme +import com.bitwarden.ui.util.Text +import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState +import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers + +/** + * Displays the common item history for the vault item screen. + * + * @param commonState The common state containing item history. + * @param vaultCommonItemTypeHandlers Provides the handlers required for the item history. + * @param loginPasswordRevisionDate The revision date for the login password (Login Cipher + * specific). + */ +@Suppress("LongMethod") +fun LazyListScope.vaultItemHistory( + commonState: VaultItemState.ViewState.Content.Common, + vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, + loginPasswordRevisionDate: Text?, +) { + item(key = "lastUpdated") { + Spacer(modifier = Modifier.height(height = 16.dp)) + Text( + text = commonState.lastUpdated(), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp) + .animateItem() + .testTag(tag = "CipherItemLastUpdated"), + ) + } + item(key = "created") { + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = commonState.created(), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp) + .animateItem() + .testTag(tag = "CipherItemCreated"), + ) + } + loginPasswordRevisionDate?.let { revisionDate -> + item(key = "revisionDate") { + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = revisionDate(), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp) + .animateItem(), + ) + } + } + commonState.passwordHistoryCount?.let { passwordHistoryCount -> + item(key = "passwordHistoryCount") { + Spacer(modifier = Modifier.height(height = 4.dp)) + BitwardenHyperTextLink( + annotatedResId = BitwardenString.password_history_count, + args = arrayOf(passwordHistoryCount.toString()), + annotationKey = "passwordHistory", + accessibilityString = stringResource(id = BitwardenString.password_history), + onClick = vaultCommonItemTypeHandlers.onPasswordHistoryClick, + style = BitwardenTheme.typography.labelMedium, + modifier = Modifier + .wrapContentWidth() + .standardHorizontalMargin() + .padding(horizontal = 12.dp) + .animateItem(), + ) + } + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemNotes.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemNotes.kt new file mode 100644 index 00000000000..eeaa812e821 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/component/VaultItemNotes.kt @@ -0,0 +1,69 @@ +package com.x8bit.bitwarden.ui.vault.feature.item.component + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton +import com.bitwarden.ui.platform.components.field.BitwardenTextField +import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.bitwarden.ui.platform.components.model.CardStyle +import com.bitwarden.ui.platform.resource.BitwardenDrawable +import com.bitwarden.ui.platform.resource.BitwardenString +import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers + +/** + * Displays the common notes field for the vault item screen. + * + * @param notes The notes. + * @param vaultCommonItemTypeHandlers Provides the handlers required for the notes. + * @param showHeader Indicates whether to show the header. + */ +fun LazyListScope.vaultItemNotes( + notes: String?, + vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, + showHeader: Boolean = true, +) { + notes ?: return + item(key = "notes") { + if (showHeader) { + Spacer(modifier = Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(id = BitwardenString.additional_options), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(horizontal = 16.dp) + .animateItem(), + ) + } + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenTextField( + label = stringResource(id = BitwardenString.notes), + value = notes, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( + vectorIconRes = BitwardenDrawable.ic_copy, + contentDescription = stringResource(id = BitwardenString.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .animateItem(), + ) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index e25ba3789d6..91527229c02 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -27,6 +27,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import java.time.Clock import java.time.format.FormatStyle import java.util.Locale @@ -53,14 +54,17 @@ fun CipherView.toViewState( common = VaultItemState.ViewState.Content.Common( currentCipher = this, name = name, - customFields = fields.orEmpty().map { fieldView -> - fieldView.toCustomField( - previousState = previousState - ?.common - ?.customFields - ?.find { it.id == fieldView.hashCode().toString() }, - ) - }, + customFields = fields + .orEmpty() + .map { fieldView -> + fieldView.toCustomField( + previousState = previousState + ?.common + ?.customFields + ?.find { it.id == fieldView.hashCode().toString() }, + ) + } + .toImmutableList(), created = BitwardenString.created.asText( creationDate.toFormattedDateTimeStyle( dateStyle = FormatStyle.MEDIUM, @@ -102,7 +106,8 @@ fun CipherView.toViewState( ) } } - .orEmpty(), + .orEmpty() + .toImmutableList(), canDelete = canDelete, canRestore = canRestore, canAssignToCollections = canAssignToCollections, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 670cbf5f343..3cbba4db675 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -728,7 +728,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { composeTestRule.onNodeWithTextAfterScroll("boolean").assertIsDisplayed() mutableStateFlow.update { currentState -> - updateCommonContent(currentState) { copy(customFields = emptyList()) } + updateCommonContent(currentState) { copy(customFields = persistentListOf()) } } composeTestRule.assertScrollableNodeDoesNotExist("CUSTOM FIELDS") @@ -749,7 +749,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { composeTestRule.onNodeWithTextAfterScroll("11 MB").assertIsDisplayed() mutableStateFlow.update { currentState -> - updateCommonContent(currentState) { copy(attachments = emptyList()) } + updateCommonContent(currentState) { copy(attachments = persistentListOf()) } } composeTestRule.assertScrollableNodeDoesNotExist("ATTACHMENTS") @@ -764,7 +764,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = EMPTY_LOGIN_VIEW_STATE.copy( common = EMPTY_COMMON.copy( - attachments = listOf( + attachments = persistentListOf( VaultItemState.ViewState.Content.Common.AttachmentItem( id = "attachment-id", displaySize = "11 MB", @@ -808,7 +808,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = EMPTY_LOGIN_VIEW_STATE.copy( common = EMPTY_COMMON.copy( - attachments = listOf( + attachments = persistentListOf( VaultItemState.ViewState.Content.Common.AttachmentItem( id = "attachment-id", displaySize = "11 MB", @@ -857,7 +857,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = EMPTY_LOGIN_VIEW_STATE.copy( common = EMPTY_COMMON.copy( - attachments = listOf(attachment), + attachments = persistentListOf(attachment), ), ), ) @@ -900,7 +900,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = EMPTY_LOGIN_VIEW_STATE.copy( common = EMPTY_COMMON.copy( - attachments = listOf(attachment), + attachments = persistentListOf(attachment), ), ), ) @@ -932,7 +932,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = typeState.copy( common = EMPTY_COMMON.copy( - customFields = listOf(textField), + customFields = persistentListOf(textField), ), ), ) @@ -971,7 +971,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = typeState.copy( common = EMPTY_COMMON.copy( - customFields = listOf(hiddenField), + customFields = persistentListOf(hiddenField), ), ), ) @@ -985,7 +985,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { mutableStateFlow.update { currentState -> updateCommonContent(currentState) { - copy(customFields = listOf(hiddenField.copy(isCopyable = false))) + copy(customFields = persistentListOf(hiddenField.copy(isCopyable = false))) } } @@ -1013,7 +1013,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = typeState.copy( common = EMPTY_COMMON.copy( - customFields = listOf(hiddenField), + customFields = persistentListOf(hiddenField), ), ), ) @@ -1048,7 +1048,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = typeState.copy( common = EMPTY_COMMON.copy( - customFields = listOf(textField), + customFields = persistentListOf(textField), ), ), ) @@ -1083,7 +1083,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = typeState.copy( common = EMPTY_COMMON.copy( - customFields = listOf(textField), + customFields = persistentListOf(textField), ), ), ) @@ -1097,7 +1097,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { mutableStateFlow.update { currentState -> updateCommonContent(currentState) { - copy(customFields = listOf(textField.copy(isCopyable = false))) + copy(customFields = persistentListOf(textField.copy(isCopyable = false))) } } @@ -1744,7 +1744,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { type = DEFAULT_IDENTITY, common = EMPTY_COMMON.copy( notes = "this is a note", - customFields = listOf(textField), + customFields = persistentListOf(textField), ), ), ) @@ -1780,7 +1780,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { type = DEFAULT_IDENTITY, common = EMPTY_COMMON.copy( notes = "this is a note", - customFields = listOf(textField), + customFields = persistentListOf(textField), ), ), ) @@ -1836,7 +1836,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { type = DEFAULT_SSH_KEY, common = EMPTY_COMMON.copy( notes = "this is a note", - customFields = listOf(textField), + customFields = persistentListOf(textField), ), ), ) @@ -2081,7 +2081,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = EMPTY_LOGIN_VIEW_STATE.copy( common = EMPTY_COMMON.copy( - customFields = listOf(linkedFieldUserName, linkedFieldsPassword), + customFields = persistentListOf(linkedFieldUserName, linkedFieldsPassword), ), ), ) @@ -2099,7 +2099,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { currentState.copy( viewState = EMPTY_LOGIN_VIEW_STATE.copy( common = EMPTY_COMMON.copy( - customFields = listOf(), + customFields = persistentListOf(), ), ), ) @@ -3343,7 +3343,7 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = created = BitwardenString.created.asText(""), lastUpdated = BitwardenString.last_edited.asText("Dec 31, 1969, 06:16 PM"), notes = "Lots of notes", - customFields = listOf( + customFields = persistentListOf( VaultItemState.ViewState.Content.Common.Custom.TextField( id = "12345", name = "text", @@ -3364,7 +3364,7 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = ), ), requiresCloneConfirmation = false, - attachments = listOf( + attachments = persistentListOf( VaultItemState.ViewState.Content.Common.AttachmentItem( id = "attachment-id", displaySize = "11 MB", @@ -3458,9 +3458,9 @@ private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common = created = BitwardenString.created.asText("Dec 1, 1969, 05:20 PM"), lastUpdated = BitwardenString.last_edited.asText("Dec 31, 1969, 06:16 PM"), notes = null, - customFields = emptyList(), + customFields = persistentListOf(), requiresCloneConfirmation = false, - attachments = emptyList(), + attachments = persistentListOf(), canDelete = true, canRestore = true, canAssignToCollections = true, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 6b0613826a5..0f723f15e07 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -1190,7 +1190,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { isEmpty = true, isPremiumUser = true, ).copy( - customFields = listOf(hiddenField), + customFields = persistentListOf(hiddenField), ), type = createLoginContent(isEmpty = true), ) @@ -1226,7 +1226,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { loginState.copy( viewState = loginViewState.copy( common = createCommonContent(isEmpty = true, isPremiumUser = true).copy( - customFields = listOf(hiddenField.copy(isVisible = true)), + customFields = persistentListOf(hiddenField.copy(isVisible = true)), ), ), ), @@ -3114,7 +3114,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { created = BitwardenString.created.asText("Dec 1, 1969, 05:20 PM"), lastUpdated = BitwardenString.last_edited.asText("Dec 31, 1969, 06:16 PM"), notes = "Lots of notes", - customFields = listOf( + customFields = persistentListOf( VaultItemState.ViewState.Content.Common.Custom.TextField( id = "12345", name = "text", @@ -3146,7 +3146,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { ), requiresCloneConfirmation = false, currentCipher = createMockCipherView(number = 1), - attachments = listOf( + attachments = persistentListOf( VaultItemState.ViewState.Content.Common.AttachmentItem( id = "attachment-id", displaySize = "11 MB", diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index 8a90290d48e..018b6f4ddee 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -175,9 +175,9 @@ fun createCommonContent( created = BitwardenString.created.asText("Jan 1, 1970, 12:16\u202FAM"), lastUpdated = BitwardenString.last_edited.asText("Jan 1, 1970, 12:16\u202FAM"), notes = null, - customFields = emptyList(), + customFields = persistentListOf(), requiresCloneConfirmation = false, - attachments = emptyList(), + attachments = persistentListOf(), canDelete = true, canRestore = true, canAssignToCollections = true, @@ -195,7 +195,7 @@ fun createCommonContent( created = BitwardenString.created.asText("Jan 1, 1970, 12:16\u202FAM"), lastUpdated = BitwardenString.last_edited.asText("Jan 1, 1970, 12:16\u202FAM"), notes = "Lots of notes", - customFields = listOf( + customFields = persistentListOf( FieldView( name = "text", value = "value", @@ -233,7 +233,7 @@ fun createCommonContent( .toCustomField(null), ), requiresCloneConfirmation = true, - attachments = listOf( + attachments = persistentListOf( VaultItemState.ViewState.Content.Common.AttachmentItem( id = "attachment-id", displaySize = "11 MB",