From 50448c195a9ff76bacf889c7a339c08c8c288d86 Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Sun, 18 Feb 2024 20:35:56 +0200 Subject: [PATCH] feat: Offer to remove origin items during a merge --- .../common/model/create/CreateRequest.kt | 9 +++ .../feature/home/vault/add/AddScreen.kt | 30 +++++++++ .../feature/home/vault/add/AddState.kt | 8 +++ .../home/vault/add/AddStateProducer.kt | 63 +++++++++++++++++-- .../provider/bitwarden/usecase/AddCipher.kt | 33 ++++++++++ .../commonMain/resources/MR/base/strings.xml | 3 + 6 files changed, 142 insertions(+), 4 deletions(-) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/create/CreateRequest.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/create/CreateRequest.kt index ded8add7..5e27c165 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/create/CreateRequest.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/create/CreateRequest.kt @@ -12,6 +12,7 @@ import kotlinx.datetime.Instant data class CreateRequest( val ownership: Ownership? = null, val ownership2: Ownership2? = null, + val merge: Merge? = null, val title: String? = null, val note: String? = null, val favorite: Boolean? = null, @@ -50,6 +51,14 @@ data class CreateRequest( companion object; } + @optics + data class Merge( + val ciphers: List, + val removeOrigin: Boolean, + ) { + companion object; + } + @optics sealed interface Attachment { companion object; diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddScreen.kt index cad3c888..ed1ff657 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddScreen.kt @@ -115,6 +115,7 @@ import com.artemchep.keyguard.ui.FlatItem import com.artemchep.keyguard.ui.FlatItemAction import com.artemchep.keyguard.ui.FlatItemLayout import com.artemchep.keyguard.ui.FlatItemTextContent +import com.artemchep.keyguard.ui.FlatSimpleNote import com.artemchep.keyguard.ui.FlatTextField import com.artemchep.keyguard.ui.FlatTextFieldBadge import com.artemchep.keyguard.ui.LeMOdelBottomSheet @@ -124,6 +125,7 @@ import com.artemchep.keyguard.ui.PasswordFlatTextField import com.artemchep.keyguard.ui.PasswordPwnedBadge import com.artemchep.keyguard.ui.PasswordStrengthBadge import com.artemchep.keyguard.ui.ScaffoldColumn +import com.artemchep.keyguard.ui.SimpleNote import com.artemchep.keyguard.ui.UrlFlatTextField import com.artemchep.keyguard.ui.button.FavouriteToggleButton import com.artemchep.keyguard.ui.icons.IconBox @@ -391,6 +393,34 @@ private fun ColumnScope.populateItemsContent( onClick = state.ownership.onClick, ) Section() + if (state.merge != null) { + ExpandedIfNotEmpty( + valueOrNull = state.merge.note, + ) { note -> + FlatSimpleNote( + modifier = Modifier, + note = note, + ) + } + FlatItemLayout( + leading = { + Checkbox( + checked = state.merge.removeOrigin.checked, + onCheckedChange = null, + ) + }, + content = { + Text( + text = stringResource(Res.strings.additem_merge_remove_origin_ciphers_title), + ) + }, + onClick = { + val newValue = !state.merge.removeOrigin.checked + state.merge.removeOrigin.onChange?.invoke(newValue) + }, + ) + Section() + } Spacer(Modifier.height(24.dp)) val logRepository by rememberInstance() remember(state) { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddState.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddState.kt index 8f887614..afb3d5c3 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddState.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddState.kt @@ -18,6 +18,7 @@ import com.artemchep.keyguard.feature.auth.common.TextFieldModel2 import com.artemchep.keyguard.feature.confirmation.organization.FolderInfo import com.artemchep.keyguard.feature.filepicker.FilePickerIntent import com.artemchep.keyguard.ui.FlatItemAction +import com.artemchep.keyguard.ui.SimpleNote import com.artemchep.keyguard.ui.icons.AccentColors import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow @@ -30,6 +31,7 @@ data class AddState( val title: String = "", val favourite: SwitchFieldModel, val ownership: Ownership, + val merge: Merge? = null, val sideEffects: SideEffects, val actions: List = emptyList(), val items: List = emptyList(), @@ -58,6 +60,12 @@ data class AddState( ) } + data class Merge( + val ciphers: List, + val note: SimpleNote?, + val removeOrigin: SwitchFieldModel, + ) + data class SaveToElement( val readOnly: Boolean, val items: List = emptyList(), diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddStateProducer.kt index d1a02377..37cf42a9 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddStateProducer.kt @@ -38,6 +38,7 @@ import com.artemchep.keyguard.common.model.Loadable import com.artemchep.keyguard.common.model.ToastMessage import com.artemchep.keyguard.common.model.TotpToken import com.artemchep.keyguard.common.model.UsernameVariation2 +import com.artemchep.keyguard.common.model.canDelete import com.artemchep.keyguard.common.model.create.CreateRequest import com.artemchep.keyguard.common.model.create.address1 import com.artemchep.keyguard.common.model.create.address2 @@ -128,6 +129,7 @@ import com.artemchep.keyguard.platform.parcelize.LeParcelize import com.artemchep.keyguard.provider.bitwarden.usecase.autofill import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.ui.FlatItemAction +import com.artemchep.keyguard.ui.SimpleNote import com.artemchep.keyguard.ui.icons.ChevronIcon import com.artemchep.keyguard.ui.icons.IconBox import com.artemchep.keyguard.ui.icons.Stub @@ -237,6 +239,39 @@ fun produceAddScreenState( getCiphers = getCiphers, ) + val mergeFlow = if (args.merge != null) { + val ciphersHaveAttachments = args.merge.ciphers.any { it.attachments.isNotEmpty() } + val note = when { + ciphersHaveAttachments -> { + val text = translate(Res.strings.additem_merge_attachments_note) + SimpleNote( + text = text, + type = SimpleNote.Type.INFO, + ) + } + + else -> null + } + + val mergeRemoveCiphers = mutablePersistedFlow("merge.remove_ciphers") { + false + } + mergeRemoveCiphers + .map { removeCiphers -> + val removeOrigin = SwitchFieldModel( + checked = removeCiphers, + onChange = mergeRemoveCiphers::value::set, + ) + AddState.Merge( + ciphers = args.merge.ciphers, + note = note, + removeOrigin = removeOrigin, + ) + } + } else { + flowOf(null) + } + val loginHolder = produceLoginState( args = args, ownershipFlow = ownershipFlow, @@ -685,7 +720,9 @@ fun produceAddScreenState( } } - val title = if (args.ownershipRo) { + val title = if (args.merge != null) { + translate(Res.strings.additem_header_merge_title) + } else if (args.ownershipRo) { translate(Res.strings.additem_header_edit_title) } else { translate(Res.strings.additem_header_new_title) @@ -742,7 +779,23 @@ fun produceAddScreenState( } f } - (populatorFlows.map { it.second } + ownershipPopulator + favouritePopulator + typePopulator) + val mergePopulator = + mergeFlow + .map { merge -> + val f = fun(r: CreateRequest): CreateRequest { + if (merge == null) { + return r + } + + val requestMerge = CreateRequest.Merge( + ciphers = merge.ciphers, + removeOrigin = merge.removeOrigin.checked, + ) + return r.copy(merge = requestMerge) + } + f + } + (populatorFlows.map { it.second } + ownershipPopulator + mergePopulator + favouritePopulator + typePopulator) .combineToList() } .map { populators -> @@ -756,10 +809,11 @@ fun produceAddScreenState( val f = combine( actions, favouriteFlow, - ownershipFlow, + ownershipFlow + .combine(mergeFlow) { a, b -> a to b }, itfff, outputFlow, - ) { q, s, c, x, request -> + ) { q, s, (c, merge), x, request -> logRepository.post( "Foo3", "create state ${x.size} (+${items1.size}) items| ${x.joinToString { it.id }}", @@ -768,6 +822,7 @@ fun produceAddScreenState( title = title, favourite = s, ownership = c, + merge = merge, actions = q, items = items1 + x, sideEffects = sideEffects, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt index 35260bef..4dc16f9d 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt @@ -6,16 +6,20 @@ import com.artemchep.keyguard.common.io.bind import com.artemchep.keyguard.common.io.combine import com.artemchep.keyguard.common.io.effectMap import com.artemchep.keyguard.common.io.flatMap +import com.artemchep.keyguard.common.io.flatTap import com.artemchep.keyguard.common.io.io import com.artemchep.keyguard.common.io.ioEffect +import com.artemchep.keyguard.common.io.ioUnit import com.artemchep.keyguard.common.io.map import com.artemchep.keyguard.common.model.AccountId import com.artemchep.keyguard.common.model.DSecret +import com.artemchep.keyguard.common.model.canDelete import com.artemchep.keyguard.common.model.create.CreateRequest import com.artemchep.keyguard.common.service.crypto.CryptoGenerator import com.artemchep.keyguard.common.usecase.AddCipher import com.artemchep.keyguard.common.usecase.AddFolder import com.artemchep.keyguard.common.usecase.GetPasswordStrength +import com.artemchep.keyguard.common.usecase.TrashCipherById import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher import com.artemchep.keyguard.core.store.bitwarden.BitwardenService import com.artemchep.keyguard.feature.confirmation.organization.FolderInfo @@ -32,6 +36,7 @@ import org.kodein.di.instance class AddCipherImpl( private val modifyDatabase: ModifyDatabase, private val addFolder: AddFolder, + private val trashCipherById: TrashCipherById, private val cryptoGenerator: CryptoGenerator, private val getPasswordStrength: GetPasswordStrength, ) : AddCipher { @@ -44,6 +49,7 @@ class AddCipherImpl( constructor(directDI: DirectDI) : this( modifyDatabase = directDI.instance(), addFolder = directDI.instance(), + trashCipherById = directDI.instance(), cryptoGenerator = directDI.instance(), getPasswordStrength = directDI.instance(), ) @@ -132,6 +138,33 @@ class AddCipherImpl( .map { it.cipherId }, ) } + }.flatTap { + val cipherIdsToTrash = cipherIdsToRequests + .values + .asSequence() + .mapNotNull { + val ciphers = it.merge?.ciphers + ?: return@mapNotNull null + // Ignore the request if we do not need to trash the + // origin ciphers. + if (!it.merge.removeOrigin) { + return@mapNotNull null + } + ciphers + } + .flatten() + .filter { cipher -> + cipher.canDelete() && + !cipher.deleted + } + .map { cipher -> + cipher.id + } + .toSet() + if (cipherIdsToTrash.isEmpty()) { + return@flatTap ioUnit() + } + trashCipherById(cipherIdsToTrash) } private fun copyLocalFilesToInternalStorageIo( diff --git a/common/src/commonMain/resources/MR/base/strings.xml b/common/src/commonMain/resources/MR/base/strings.xml index 97e9b7b9..cd91d6d6 100644 --- a/common/src/commonMain/resources/MR/base/strings.xml +++ b/common/src/commonMain/resources/MR/base/strings.xml @@ -560,6 +560,9 @@ New item Edit item + Merge items + Move origin items to trash + Merging items does not merge their attachments! The created item will not have any attachments added. Style text with %1$s or %2$s and more. Limited Markdown syntax is supported. italic bold