feat: Hide accounts from the Main screens #35
This commit is contained in:
parent
e8bd9b2e74
commit
462c13643a
|
@ -26,7 +26,9 @@ import androidx.lifecycle.lifecycleScope
|
|||
import com.artemchep.keyguard.common.model.MasterSession
|
||||
import com.artemchep.keyguard.common.model.VaultState
|
||||
import com.artemchep.keyguard.common.usecase.GetCiphers
|
||||
import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||
import com.artemchep.keyguard.common.usecase.GetVaultSession
|
||||
import com.artemchep.keyguard.common.usecase.filterHiddenProfiles
|
||||
import com.artemchep.keyguard.feature.keyguard.ManualAppScreen
|
||||
import com.artemchep.keyguard.feature.keyguard.ManualAppScreenOnCreate
|
||||
import com.artemchep.keyguard.feature.keyguard.ManualAppScreenOnLoading
|
||||
|
@ -179,7 +181,14 @@ class PasskeyGetUnlockActivity : BaseActivity(), DIAware {
|
|||
}
|
||||
val ciphers = kotlin.run {
|
||||
val getCiphers = session.di.direct.instance<GetCiphers>()
|
||||
getCiphers()
|
||||
val getProfiles = session.di.direct.instance<GetProfiles>()
|
||||
|
||||
val ciphersRawFlow = filterHiddenProfiles(
|
||||
getProfiles = getProfiles,
|
||||
getCiphers = getCiphers,
|
||||
filter = null,
|
||||
)
|
||||
ciphersRawFlow
|
||||
.first()
|
||||
}
|
||||
|
||||
|
|
|
@ -65,10 +65,16 @@ class KeyguardAutofillService : AutofillService(), DIAware {
|
|||
when (session) {
|
||||
is MasterSession.Key -> {
|
||||
val getCiphers = session.di.direct.instance<GetCiphers>()
|
||||
getCiphers()
|
||||
val getProfiles = session.di.direct.instance<GetProfiles>()
|
||||
val ciphersRawFlow = filterHiddenProfiles(
|
||||
getProfiles = getProfiles,
|
||||
getCiphers = getCiphers,
|
||||
filter = null,
|
||||
)
|
||||
ciphersRawFlow
|
||||
.map { ciphers ->
|
||||
ciphers
|
||||
.filter { it.deletedDate == null }
|
||||
.filter { !it.deleted }
|
||||
.right()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,9 @@ import com.artemchep.keyguard.common.io.launchIn
|
|||
import com.artemchep.keyguard.common.model.MasterSession
|
||||
import com.artemchep.keyguard.common.usecase.GetCanWrite
|
||||
import com.artemchep.keyguard.common.usecase.GetCiphers
|
||||
import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||
import com.artemchep.keyguard.common.usecase.GetVaultSession
|
||||
import com.artemchep.keyguard.common.usecase.filterHiddenProfiles
|
||||
import com.artemchep.keyguard.feature.crashlytics.crashlyticsTap
|
||||
import com.artemchep.keyguard.platform.recordLog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -156,11 +158,17 @@ class KeyguardCredentialService : CredentialProviderService(), DIAware {
|
|||
val cipherHistoryOpenedRepository =
|
||||
session.di.direct.instance<CipherHistoryOpenedRepository>()
|
||||
val getCiphers = session.di.direct.instance<GetCiphers>()
|
||||
val getProfiles = session.di.direct.instance<GetProfiles>()
|
||||
|
||||
val ciphers = getCiphers()
|
||||
val ciphersRawFlow = filterHiddenProfiles(
|
||||
getProfiles = getProfiles,
|
||||
getCiphers = getCiphers,
|
||||
filter = null,
|
||||
)
|
||||
val ciphers = ciphersRawFlow
|
||||
.map { ciphers ->
|
||||
ciphers
|
||||
.filter { it.deletedDate == null }
|
||||
.filter { !it.deleted }
|
||||
}
|
||||
.first()
|
||||
val response = ioEffect {
|
||||
|
|
|
@ -14,6 +14,11 @@ data class DProfile(
|
|||
val accentColor: AccentColors,
|
||||
val name: String,
|
||||
val premium: Boolean,
|
||||
/**
|
||||
* `true` if the account should be hidden from the main screens and
|
||||
* not used during the autofill process, `false` otherwise.
|
||||
*/
|
||||
val hidden: Boolean,
|
||||
val securityStamp: String?,
|
||||
val twoFactorEnabled: Boolean,
|
||||
val masterPasswordHint: String?,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package com.artemchep.keyguard.common.model
|
||||
|
||||
data class PutProfileHiddenRequest(
|
||||
val patch: Map<String, Boolean>,
|
||||
)
|
|
@ -1,10 +1,86 @@
|
|||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.model.DFilter
|
||||
import com.artemchep.keyguard.common.model.DSecret
|
||||
import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.common.model.DSendFilter
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Provides a list of all available on the
|
||||
* device accounts.
|
||||
*/
|
||||
interface GetCiphers : () -> Flow<List<DSecret>>
|
||||
|
||||
fun filterHiddenProfiles(
|
||||
getCiphers: GetCiphers,
|
||||
getProfiles: GetProfiles,
|
||||
filter: DFilter? = null,
|
||||
): Flow<List<DSecret>> {
|
||||
val shouldFilter = filter
|
||||
?.let { f ->
|
||||
// If we have a pre-set filter that specifies an
|
||||
// identifier of the folder or collection or account etc,
|
||||
// then we ignore the hidden account setting.
|
||||
DFilter.findOne<DFilter.ById>(f) { true } == null
|
||||
}
|
||||
?: true
|
||||
return if (shouldFilter) {
|
||||
_filterHiddenProfiles(
|
||||
getItems = getCiphers,
|
||||
getProfiles = getProfiles,
|
||||
getAccount = { it.accountId },
|
||||
)
|
||||
} else {
|
||||
getCiphers()
|
||||
}
|
||||
}
|
||||
|
||||
fun filterHiddenProfiles(
|
||||
getSends: GetSends,
|
||||
getProfiles: GetProfiles,
|
||||
filter: DSendFilter? = null,
|
||||
): Flow<List<DSend>> {
|
||||
val shouldFilter = filter
|
||||
?.let { f ->
|
||||
// If we have a pre-set filter that specifies an
|
||||
// identifier of the folder or collection or account etc,
|
||||
// then we ignore the hidden account setting.
|
||||
DSendFilter.findOne<DSendFilter.ById>(f) { true } == null
|
||||
}
|
||||
?: true
|
||||
return if (shouldFilter) {
|
||||
_filterHiddenProfiles(
|
||||
getItems = getSends,
|
||||
getProfiles = getProfiles,
|
||||
getAccount = { it.accountId },
|
||||
)
|
||||
} else {
|
||||
getSends()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> _filterHiddenProfiles(
|
||||
getItems: () -> Flow<List<T>>,
|
||||
getProfiles: GetProfiles,
|
||||
crossinline getAccount: (T) -> String,
|
||||
): Flow<List<T>> {
|
||||
val ccc = getProfiles()
|
||||
.map { profiles ->
|
||||
profiles
|
||||
.asSequence()
|
||||
.filter { it.hidden }
|
||||
.map { it.accountId }
|
||||
.toPersistentSet()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
return getItems()
|
||||
.combine(ccc) { items, hiddenAccountIds ->
|
||||
items
|
||||
.filter { getAccount(it) !in hiddenAccountIds }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.model.PutProfileHiddenRequest
|
||||
|
||||
interface PutProfileHidden : (
|
||||
PutProfileHiddenRequest,
|
||||
) -> IO<Boolean>
|
|
@ -87,6 +87,7 @@ import com.artemchep.keyguard.common.usecase.PatchWatchtowerAlertCipher
|
|||
import com.artemchep.keyguard.common.usecase.PutAccountColorById
|
||||
import com.artemchep.keyguard.common.usecase.PutAccountMasterPasswordHintById
|
||||
import com.artemchep.keyguard.common.usecase.PutAccountNameById
|
||||
import com.artemchep.keyguard.common.usecase.PutProfileHidden
|
||||
import com.artemchep.keyguard.common.usecase.RePromptCipherById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveAccountById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveAccounts
|
||||
|
@ -187,6 +188,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.PatchWatchtowerAlertCip
|
|||
import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountColorByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountMasterPasswordHintByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountNameByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.PutProfileHiddenImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RePromptCipherByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveAccountByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveAccountsImpl
|
||||
|
@ -417,6 +419,9 @@ fun DI.Builder.createSubDi2(
|
|||
bindSingleton<PutAccountNameById> {
|
||||
PutAccountNameByIdImpl(this)
|
||||
}
|
||||
bindSingleton<PutProfileHidden> {
|
||||
PutProfileHiddenImpl(this)
|
||||
}
|
||||
bindSingleton<GetGeneratorHistory> {
|
||||
GetGeneratorHistoryImpl(
|
||||
directDI = this,
|
||||
|
|
|
@ -21,6 +21,8 @@ data class BitwardenProfile(
|
|||
val email: String,
|
||||
val emailVerified: Boolean,
|
||||
val premium: Boolean,
|
||||
// Keyguard-specific field
|
||||
val hidden: Boolean = false,
|
||||
val securityStamp: String = "",
|
||||
val twoFactorEnabled: Boolean,
|
||||
val masterPasswordHint: String?,
|
||||
|
|
|
@ -5,7 +5,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import arrow.optics.optics
|
||||
import com.artemchep.keyguard.common.model.DAccount
|
||||
import com.artemchep.keyguard.feature.home.vault.model.VaultViewItem
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.ContextItem
|
||||
|
||||
@Immutable
|
||||
@optics
|
||||
|
@ -26,7 +26,7 @@ data class AccountViewState(
|
|||
data class Data(
|
||||
val data: DAccount,
|
||||
val items: List<VaultViewItem>,
|
||||
val actions: List<FlatItemAction>,
|
||||
val actions: List<ContextItem>,
|
||||
val primaryAction: PrimaryAction? = null,
|
||||
val onOpenWebVault: (() -> Unit)? = null,
|
||||
) : Content {
|
||||
|
|
|
@ -11,14 +11,18 @@ import androidx.compose.material.icons.outlined.Edit
|
|||
import androidx.compose.material.icons.outlined.Fingerprint
|
||||
import androidx.compose.material.icons.outlined.Folder
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.HideSource
|
||||
import androidx.compose.material.icons.outlined.Keyboard
|
||||
import androidx.compose.material.icons.outlined.Login
|
||||
import androidx.compose.material.icons.outlined.Logout
|
||||
import androidx.compose.material.icons.outlined.VerifiedUser
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -36,6 +40,7 @@ import com.artemchep.keyguard.common.model.DAccount
|
|||
import com.artemchep.keyguard.common.model.DFilter
|
||||
import com.artemchep.keyguard.common.model.DMeta
|
||||
import com.artemchep.keyguard.common.model.DProfile
|
||||
import com.artemchep.keyguard.common.model.PutProfileHiddenRequest
|
||||
import com.artemchep.keyguard.common.model.firstOrNull
|
||||
import com.artemchep.keyguard.common.service.clipboard.ClipboardService
|
||||
import com.artemchep.keyguard.common.usecase.CopyText
|
||||
|
@ -52,6 +57,7 @@ import com.artemchep.keyguard.common.usecase.GetProfiles
|
|||
import com.artemchep.keyguard.common.usecase.PutAccountColorById
|
||||
import com.artemchep.keyguard.common.usecase.PutAccountMasterPasswordHintById
|
||||
import com.artemchep.keyguard.common.usecase.PutAccountNameById
|
||||
import com.artemchep.keyguard.common.usecase.PutProfileHidden
|
||||
import com.artemchep.keyguard.common.usecase.QueueSyncById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveAccountById
|
||||
import com.artemchep.keyguard.common.usecase.SupervisorRead
|
||||
|
@ -117,6 +123,7 @@ fun accountState(
|
|||
putAccountNameById = instance(),
|
||||
putAccountColorById = instance(),
|
||||
putAccountMasterPasswordHintById = instance(),
|
||||
putProfileHidden = instance(),
|
||||
getAccounts = instance(),
|
||||
getProfiles = instance(),
|
||||
getCiphers = instance(),
|
||||
|
@ -148,6 +155,7 @@ fun accountState(
|
|||
putAccountNameById: PutAccountNameById,
|
||||
putAccountColorById: PutAccountColorById,
|
||||
putAccountMasterPasswordHintById: PutAccountMasterPasswordHintById,
|
||||
putProfileHidden: PutProfileHidden,
|
||||
getAccounts: GetAccounts,
|
||||
getProfiles: GetProfiles,
|
||||
getCiphers: GetCiphers,
|
||||
|
@ -203,6 +211,16 @@ fun accountState(
|
|||
navigate(intent)
|
||||
}
|
||||
|
||||
fun doHideProfileById(profileId: String, hidden: Boolean) {
|
||||
val request = PutProfileHiddenRequest(
|
||||
patch = mapOf(
|
||||
profileId to hidden,
|
||||
),
|
||||
)
|
||||
putProfileHidden(request)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
val accountFlow = getAccounts()
|
||||
.map { it.firstOrNull(accountId) }
|
||||
launchAutoPopSelfHandler(accountFlow)
|
||||
|
@ -317,48 +335,88 @@ fun accountState(
|
|||
getGravatarUrl = getGravatarUrl,
|
||||
).toList()
|
||||
}
|
||||
val actionsFlow = combine(
|
||||
accountFlow,
|
||||
profileFlow,
|
||||
) { accountOrNull, profileOrNull ->
|
||||
val syncing = false
|
||||
val busy = false
|
||||
buildContextItems {
|
||||
section {
|
||||
if (accountOrNull == null) {
|
||||
return@section
|
||||
}
|
||||
|
||||
this += FlatItemAction(
|
||||
leading = {
|
||||
SyncIcon(rotating = syncing)
|
||||
},
|
||||
title = translate(Res.strings.sync),
|
||||
onClick = if (busy) {
|
||||
null
|
||||
} else {
|
||||
// on click listener
|
||||
::doSyncAccountById.partially1(accountOrNull.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
section {
|
||||
if (profileOrNull == null) {
|
||||
return@section
|
||||
}
|
||||
|
||||
val hidden = profileOrNull.hidden
|
||||
|
||||
val onCheckedChange = ::doHideProfileById
|
||||
.partially1(profileOrNull.profileId)
|
||||
this += FlatItemAction(
|
||||
leading = {
|
||||
Icon(
|
||||
Icons.Outlined.VisibilityOff,
|
||||
null,
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
Switch(
|
||||
checked = hidden,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
},
|
||||
title = translate(Res.strings.account_action_hide_title),
|
||||
text = translate(Res.strings.account_action_hide_text),
|
||||
onClick = onCheckedChange.partially1(!hidden),
|
||||
)
|
||||
}
|
||||
section {
|
||||
if (accountOrNull == null) {
|
||||
return@section
|
||||
}
|
||||
|
||||
this += FlatItemAction(
|
||||
icon = Icons.Outlined.Logout,
|
||||
title = translate(Res.strings.account_action_sign_out_title),
|
||||
onClick = if (busy) {
|
||||
null
|
||||
} else {
|
||||
// on click listener
|
||||
::doRemoveAccountById.partially1(accountOrNull.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
combine(
|
||||
accountFlow,
|
||||
actionsFlow,
|
||||
itemsFlow,
|
||||
primaryActionFlow,
|
||||
) { accountOrNull, items, primaryAction ->
|
||||
) { accountOrNull, actions, items, primaryAction ->
|
||||
if (accountOrNull == null) {
|
||||
return@combine AccountViewState.Content.NotFound
|
||||
}
|
||||
|
||||
val syncing = false
|
||||
val busy = false
|
||||
|
||||
val actionSync =
|
||||
FlatItemAction(
|
||||
leading = {
|
||||
SyncIcon(rotating = syncing)
|
||||
},
|
||||
title = translate(Res.strings.sync),
|
||||
onClick = if (busy) {
|
||||
null
|
||||
} else {
|
||||
// on click listener
|
||||
::doSyncAccountById.partially1(accountOrNull.id)
|
||||
},
|
||||
)
|
||||
val actionRemove =
|
||||
FlatItemAction(
|
||||
icon = Icons.Outlined.Logout,
|
||||
title = translate(Res.strings.account_action_sign_out_title),
|
||||
onClick = if (busy) {
|
||||
null
|
||||
} else {
|
||||
// on click listener
|
||||
::doRemoveAccountById.partially1(accountOrNull.id)
|
||||
},
|
||||
)
|
||||
AccountViewState.Content.Data(
|
||||
data = accountOrNull,
|
||||
actions = listOf(
|
||||
actionSync,
|
||||
actionRemove,
|
||||
),
|
||||
actions = actions,
|
||||
items = items,
|
||||
primaryAction = primaryAction,
|
||||
onOpenWebVault = {
|
||||
|
|
|
@ -24,18 +24,17 @@ import com.artemchep.keyguard.common.usecase.GetCiphers
|
|||
import com.artemchep.keyguard.common.usecase.GetCollections
|
||||
import com.artemchep.keyguard.common.usecase.GetConcealFields
|
||||
import com.artemchep.keyguard.common.usecase.GetOrganizations
|
||||
import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||
import com.artemchep.keyguard.common.usecase.GetTotpCode
|
||||
import com.artemchep.keyguard.common.usecase.GetWebsiteIcons
|
||||
import com.artemchep.keyguard.common.usecase.filterHiddenProfiles
|
||||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
import com.artemchep.keyguard.feature.attachments.SelectableItemState
|
||||
import com.artemchep.keyguard.feature.attachments.SelectableItemStateRaw
|
||||
import com.artemchep.keyguard.feature.confirmation.elevatedaccess.createElevatedAccessDialogIntent
|
||||
import com.artemchep.keyguard.feature.duplicates.DuplicatesRoute
|
||||
import com.artemchep.keyguard.feature.generator.history.mapLatestScoped
|
||||
import com.artemchep.keyguard.feature.generator.wordlist.WordlistsRoute
|
||||
import com.artemchep.keyguard.feature.home.vault.component.VaultListItem
|
||||
import com.artemchep.keyguard.feature.home.vault.model.VaultItem2
|
||||
import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon
|
||||
import com.artemchep.keyguard.feature.home.vault.screen.VaultViewRoute
|
||||
import com.artemchep.keyguard.feature.home.vault.screen.toVaultListItem
|
||||
import com.artemchep.keyguard.feature.home.vault.screen.verify
|
||||
|
@ -70,7 +69,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.compose.localDI
|
||||
|
@ -103,6 +101,7 @@ fun produceDuplicatesListState(
|
|||
getOrganizations = instance(),
|
||||
getCollections = instance(),
|
||||
getCiphers = instance(),
|
||||
getProfiles = instance(),
|
||||
getCanWrite = instance(),
|
||||
cipherToolbox = instance(),
|
||||
cipherDuplicatesCheck = instance(),
|
||||
|
@ -121,6 +120,7 @@ fun produceDuplicatesListState(
|
|||
getOrganizations: GetOrganizations,
|
||||
getCollections: GetCollections,
|
||||
getCiphers: GetCiphers,
|
||||
getProfiles: GetProfiles,
|
||||
getCanWrite: GetCanWrite,
|
||||
cipherToolbox: CipherToolbox,
|
||||
cipherDuplicatesCheck: CipherDuplicatesCheck,
|
||||
|
@ -203,7 +203,12 @@ fun produceDuplicatesListState(
|
|||
if (r == 0) r = a.id.compareTo(b.id)
|
||||
r
|
||||
}
|
||||
val ciphersFlow = getCiphers()
|
||||
val ciphersRawFlow = filterHiddenProfiles(
|
||||
getProfiles = getProfiles,
|
||||
getCiphers = getCiphers,
|
||||
filter = args.filter,
|
||||
)
|
||||
val ciphersFlow = ciphersRawFlow
|
||||
.map { ciphers ->
|
||||
ciphers
|
||||
.filter { it.deletedDate == null }
|
||||
|
|
|
@ -254,6 +254,7 @@ fun accountListScreenState(
|
|||
title = AnnotatedString(it.username),
|
||||
text = it.host,
|
||||
error = error,
|
||||
hidden = profile?.hidden == true,
|
||||
syncing = syncing,
|
||||
selecting = selectionMode,
|
||||
actions = listOf(),
|
||||
|
|
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
@ -32,6 +33,7 @@ import androidx.compose.ui.unit.dp
|
|||
import com.artemchep.keyguard.feature.home.settings.accounts.model.AccountItem
|
||||
import com.artemchep.keyguard.feature.home.vault.component.Section
|
||||
import com.artemchep.keyguard.feature.home.vault.component.rememberSecretAccentColor
|
||||
import com.artemchep.keyguard.ui.AvatarBadgeIcon
|
||||
import com.artemchep.keyguard.ui.AvatarBuilder
|
||||
import com.artemchep.keyguard.ui.ExpandedIfNotEmptyForRow
|
||||
import com.artemchep.keyguard.ui.FlatItemLayout
|
||||
|
@ -85,7 +87,11 @@ fun AccountListItemText(
|
|||
accent = accent,
|
||||
active = true,
|
||||
badge = {
|
||||
// Do nothing.
|
||||
if (item.hidden) {
|
||||
AvatarBadgeIcon(
|
||||
imageVector = Icons.Outlined.VisibilityOff,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ sealed interface AccountItem {
|
|||
val title: AnnotatedString,
|
||||
val text: String?,
|
||||
val error: Boolean = false,
|
||||
val hidden: Boolean,
|
||||
val syncing: Boolean = false,
|
||||
val selecting: Boolean = false,
|
||||
val actionNeeded: Boolean,
|
||||
|
|
|
@ -74,6 +74,7 @@ import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon
|
|||
import com.artemchep.keyguard.feature.localization.textResource
|
||||
import com.artemchep.keyguard.feature.twopane.LocalHasDetailPane
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.AvatarBadgeIcon
|
||||
import com.artemchep.keyguard.ui.AvatarBuilder
|
||||
import com.artemchep.keyguard.ui.DisabledEmphasisAlpha
|
||||
import com.artemchep.keyguard.ui.DropdownMenuItemFlat
|
||||
|
@ -840,30 +841,18 @@ fun AccountListItemTextIcon(
|
|||
active = item.source.service.remote != null,
|
||||
badge = {
|
||||
if (item.favourite) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(1.dp),
|
||||
AvatarBadgeIcon(
|
||||
imageVector = Icons.Outlined.KeyguardFavourite,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
if (item.source.reprompt) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(1.dp),
|
||||
AvatarBadgeIcon(
|
||||
imageVector = Icons.Outlined.Lock,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
if (item.attachments) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(1.dp),
|
||||
AvatarBadgeIcon(
|
||||
imageVector = Icons.Outlined.KeyguardAttachment,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -60,6 +60,7 @@ import com.artemchep.keyguard.common.usecase.PasskeyTargetCheck
|
|||
import com.artemchep.keyguard.common.usecase.QueueSyncAll
|
||||
import com.artemchep.keyguard.common.usecase.RenameFolderById
|
||||
import com.artemchep.keyguard.common.usecase.SupervisorRead
|
||||
import com.artemchep.keyguard.common.usecase.filterHiddenProfiles
|
||||
import com.artemchep.keyguard.common.util.StringComparatorIgnoreCase
|
||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
|
@ -314,7 +315,12 @@ fun vaultListScreenState(
|
|||
}
|
||||
|
||||
val copy = copy(clipboardService)
|
||||
val ciphersRawFlow = getCiphers()
|
||||
|
||||
val ciphersRawFlow = filterHiddenProfiles(
|
||||
getProfiles = getProfiles,
|
||||
getCiphers = getCiphers,
|
||||
filter = args.filter,
|
||||
)
|
||||
|
||||
val querySink = mutablePersistedFlow("query") { "" }
|
||||
val queryState = mutableComposeState(querySink)
|
||||
|
@ -326,7 +332,7 @@ fun vaultListScreenState(
|
|||
val selectionHandle = selectionHandle("selection")
|
||||
// Automatically remove selection from ciphers
|
||||
// that do not exist anymore.
|
||||
getCiphers()
|
||||
ciphersRawFlow
|
||||
.onEach { ciphers ->
|
||||
val selectedItemIds = selectionHandle.idsFlow.value
|
||||
val filteredSelectedItemIds = selectedItemIds
|
||||
|
|
|
@ -20,11 +20,13 @@ import com.artemchep.keyguard.common.usecase.GetConcealFields
|
|||
import com.artemchep.keyguard.common.usecase.GetFolders
|
||||
import com.artemchep.keyguard.common.usecase.GetOrganizations
|
||||
import com.artemchep.keyguard.common.usecase.GetPasswordStrength
|
||||
import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||
import com.artemchep.keyguard.common.usecase.GetSuggestions
|
||||
import com.artemchep.keyguard.common.usecase.GetTotpCode
|
||||
import com.artemchep.keyguard.common.usecase.GetWebsiteIcons
|
||||
import com.artemchep.keyguard.common.usecase.QueueSyncAll
|
||||
import com.artemchep.keyguard.common.usecase.SupervisorRead
|
||||
import com.artemchep.keyguard.common.usecase.filterHiddenProfiles
|
||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||
import com.artemchep.keyguard.feature.attachments.SelectableItemState
|
||||
import com.artemchep.keyguard.feature.home.vault.model.VaultItem2
|
||||
|
@ -58,6 +60,7 @@ fun vaultRecentScreenState(
|
|||
getAccounts = instance(),
|
||||
getCanWrite = instance(),
|
||||
getCiphers = instance(),
|
||||
getProfiles = instance(),
|
||||
getFolders = instance(),
|
||||
getCollections = instance(),
|
||||
getOrganizations = instance(),
|
||||
|
@ -84,6 +87,7 @@ fun vaultRecentScreenState(
|
|||
getAccounts: GetAccounts,
|
||||
getCanWrite: GetCanWrite,
|
||||
getCiphers: GetCiphers,
|
||||
getProfiles: GetProfiles,
|
||||
getFolders: GetFolders,
|
||||
getCollections: GetCollections,
|
||||
getOrganizations: GetOrganizations,
|
||||
|
@ -152,6 +156,11 @@ fun vaultRecentScreenState(
|
|||
val websiteIcons: Boolean,
|
||||
)
|
||||
|
||||
val ciphersRawFlow = filterHiddenProfiles(
|
||||
getProfiles = getProfiles,
|
||||
getCiphers = getCiphers,
|
||||
filter = null,
|
||||
)
|
||||
val configFlow = combine(
|
||||
getConcealFields(),
|
||||
getAppIcons(),
|
||||
|
@ -182,7 +191,7 @@ fun vaultRecentScreenState(
|
|||
.map { it.cipherId }
|
||||
.toSet()
|
||||
}
|
||||
.combine(getCiphers()) { recents, ciphers ->
|
||||
.combine(ciphersRawFlow) { recents, ciphers ->
|
||||
recents
|
||||
.mapNotNull { recentId ->
|
||||
ciphers
|
||||
|
|
|
@ -21,7 +21,6 @@ import com.artemchep.keyguard.common.model.AccountTask
|
|||
import com.artemchep.keyguard.common.model.DSecret
|
||||
import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.common.model.iconImageVector
|
||||
import com.artemchep.keyguard.common.model.title
|
||||
import com.artemchep.keyguard.common.model.titleH
|
||||
import com.artemchep.keyguard.common.service.clipboard.ClipboardService
|
||||
import com.artemchep.keyguard.common.usecase.CipherToolbox
|
||||
|
@ -30,11 +29,13 @@ import com.artemchep.keyguard.common.usecase.DateFormatter
|
|||
import com.artemchep.keyguard.common.usecase.GetAccounts
|
||||
import com.artemchep.keyguard.common.usecase.GetAppIcons
|
||||
import com.artemchep.keyguard.common.usecase.GetCanWrite
|
||||
import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||
import com.artemchep.keyguard.common.usecase.GetSends
|
||||
import com.artemchep.keyguard.common.usecase.GetSuggestions
|
||||
import com.artemchep.keyguard.common.usecase.GetWebsiteIcons
|
||||
import com.artemchep.keyguard.common.usecase.QueueSyncAll
|
||||
import com.artemchep.keyguard.common.usecase.SupervisorRead
|
||||
import com.artemchep.keyguard.common.usecase.filterHiddenProfiles
|
||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
import com.artemchep.keyguard.feature.attachments.SelectableItemState
|
||||
|
@ -99,7 +100,6 @@ import org.kodein.di.DirectDI
|
|||
import org.kodein.di.compose.localDI
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@LeParcelize
|
||||
|
@ -151,6 +151,7 @@ fun sendListScreenState(
|
|||
getAccounts = instance(),
|
||||
getCanWrite = instance(),
|
||||
getSends = instance(),
|
||||
getProfiles = instance(),
|
||||
getAppIcons = instance(),
|
||||
getWebsiteIcons = instance(),
|
||||
toolbox = instance(),
|
||||
|
@ -173,6 +174,7 @@ fun sendListScreenState(
|
|||
getAccounts: GetAccounts,
|
||||
getCanWrite: GetCanWrite,
|
||||
getSends: GetSends,
|
||||
getProfiles: GetProfiles,
|
||||
getAppIcons: GetAppIcons,
|
||||
getWebsiteIcons: GetWebsiteIcons,
|
||||
toolbox: CipherToolbox,
|
||||
|
@ -195,15 +197,11 @@ fun sendListScreenState(
|
|||
}
|
||||
|
||||
val copy = copy(clipboardService)
|
||||
val ciphersRawFlow = getSends()
|
||||
// .map { l ->
|
||||
// Log.e("???","lol ciphers")
|
||||
// (0..100).flatMap { l }.map {
|
||||
// it.copy(id = UUID.randomUUID().toString())
|
||||
// }.also {
|
||||
// Log.d("??? search", "${it.size} items")
|
||||
// }
|
||||
// }
|
||||
val ciphersRawFlow = filterHiddenProfiles(
|
||||
getProfiles = getProfiles,
|
||||
getSends = getSends,
|
||||
filter = null,
|
||||
)
|
||||
|
||||
val querySink = mutablePersistedFlow("query") { "" }
|
||||
val queryState = mutableComposeState(querySink)
|
||||
|
@ -215,7 +213,7 @@ fun sendListScreenState(
|
|||
val selectionHandle = selectionHandle("selection")
|
||||
// Automatically remove selection from ciphers
|
||||
// that do not exist anymore.
|
||||
getSends()
|
||||
ciphersRawFlow
|
||||
.onEach { ciphers ->
|
||||
val selectedItemIds = selectionHandle.idsFlow.value
|
||||
val filteredSelectedItemIds = selectedItemIds
|
||||
|
|
|
@ -70,6 +70,7 @@ import com.artemchep.keyguard.feature.send.view.SendViewRoute
|
|||
import com.artemchep.keyguard.feature.twopane.LocalHasDetailPane
|
||||
import com.artemchep.keyguard.feature.twopane.TwoPaneScreen
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.AvatarBadgeIcon
|
||||
import com.artemchep.keyguard.ui.AvatarBuilder
|
||||
import com.artemchep.keyguard.ui.CollectedEffect
|
||||
import com.artemchep.keyguard.ui.Compose
|
||||
|
@ -674,12 +675,8 @@ fun AccountListItemTextIcon(
|
|||
active = active,
|
||||
badge = {
|
||||
if (item.hasPassword) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(1.dp),
|
||||
AvatarBadgeIcon(
|
||||
imageVector = Icons.Outlined.Key,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.artemchep.keyguard.common.usecase.GetCollections
|
|||
import com.artemchep.keyguard.common.usecase.GetFolders
|
||||
import com.artemchep.keyguard.common.usecase.GetOrganizations
|
||||
import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||
import com.artemchep.keyguard.common.usecase.filterHiddenProfiles
|
||||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
import com.artemchep.keyguard.feature.crashlytics.crashlyticsMap
|
||||
import com.artemchep.keyguard.feature.duplicates.DuplicatesRoute
|
||||
|
@ -101,7 +102,12 @@ fun produceWatchtowerState(
|
|||
PersistedStorage.InDisk(disk)
|
||||
}
|
||||
|
||||
val ciphersFlow = getCiphers()
|
||||
val ciphersRawFlow = filterHiddenProfiles(
|
||||
getProfiles = getProfiles,
|
||||
getCiphers = getCiphers,
|
||||
filter = null,
|
||||
)
|
||||
val ciphersFlow = ciphersRawFlow
|
||||
.map { secrets ->
|
||||
secrets
|
||||
.filter { secret -> !secret.deleted }
|
||||
|
|
|
@ -190,7 +190,11 @@ class SyncEngine(
|
|||
}
|
||||
|
||||
// Insert updated profile.
|
||||
if (existingProfile?.data_ != newProfile) {
|
||||
val newMergedProfile = merge(
|
||||
remote = newProfile,
|
||||
local = existingProfile?.data_,
|
||||
)
|
||||
if (newMergedProfile != existingProfile?.data_) {
|
||||
profileDao.insert(
|
||||
profileId = newProfile.profileId,
|
||||
accountId = newProfile.accountId,
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.artemchep.keyguard.common.io.parallel
|
|||
import com.artemchep.keyguard.common.service.logging.LogLevel
|
||||
import com.artemchep.keyguard.common.usecase.GetPasswordStrength
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenProfile
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenService
|
||||
import com.artemchep.keyguard.provider.bitwarden.sync.SyncManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -92,6 +93,16 @@ suspend fun merge(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun merge(
|
||||
remote: BitwardenProfile,
|
||||
local: BitwardenProfile?,
|
||||
): BitwardenProfile {
|
||||
val hidden = local?.hidden == true
|
||||
return remote.copy(
|
||||
hidden = hidden,
|
||||
)
|
||||
}
|
||||
|
||||
interface RemotePutScope<Remote> {
|
||||
fun updateRemoteModel(remote: Remote)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ fun BitwardenProfile.toDomain(
|
|||
accountUrl = accountUrl,
|
||||
name = name,
|
||||
premium = premium,
|
||||
hidden = hidden,
|
||||
securityStamp = securityStamp,
|
||||
twoFactorEnabled = twoFactorEnabled,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package com.artemchep.keyguard.provider.bitwarden.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.io.flatten
|
||||
import com.artemchep.keyguard.common.io.ioEffect
|
||||
import com.artemchep.keyguard.common.io.map
|
||||
import com.artemchep.keyguard.common.model.PutProfileHiddenRequest
|
||||
import com.artemchep.keyguard.common.usecase.PutProfileHidden
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenProfile
|
||||
import com.artemchep.keyguard.core.store.bitwarden.hidden
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyProfileById
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
/**
|
||||
* @author Artem Chepurnyi
|
||||
*/
|
||||
class PutProfileHiddenImpl(
|
||||
private val modifyProfileById: ModifyProfileById,
|
||||
) : PutProfileHidden {
|
||||
constructor(directDI: DirectDI) : this(
|
||||
modifyProfileById = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(
|
||||
request: PutProfileHiddenRequest,
|
||||
): IO<Boolean> = ioEffect {
|
||||
val profileIds = request.patch.keys
|
||||
modifyProfileById(
|
||||
profileIds,
|
||||
) { model ->
|
||||
var new = model
|
||||
val hidden = request.patch[model.profileId]
|
||||
?: return@modifyProfileById new
|
||||
new = new.copy(
|
||||
data_ = BitwardenProfile.hidden.set(new.data_, hidden),
|
||||
)
|
||||
new
|
||||
}
|
||||
// Report that we have actually modified the
|
||||
// profiles.
|
||||
.map { changedCipherIds ->
|
||||
true
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
}
|
|
@ -23,6 +23,7 @@ class ModifyProfileById(
|
|||
|
||||
operator fun invoke(
|
||||
profileIds: Set<String>,
|
||||
checkIfChanged: Boolean = true,
|
||||
transform: suspend (Profile) -> Profile,
|
||||
): IO<Unit> = modifyDatabase { database ->
|
||||
val dao = database.profileQueries
|
||||
|
@ -33,12 +34,20 @@ class ModifyProfileById(
|
|||
.filter {
|
||||
it.profileId in profileIds
|
||||
}
|
||||
.map { model ->
|
||||
.mapNotNull { model ->
|
||||
var new = model
|
||||
new = transform(new)
|
||||
// If the profile was not changed, then we do not need to
|
||||
// update it in the database.
|
||||
if (checkIfChanged && new == model) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
new
|
||||
}
|
||||
require(models.isNotEmpty())
|
||||
if (models.isEmpty()) {
|
||||
return@modifyDatabase ModifyDatabase.Result.unit()
|
||||
}
|
||||
dao.transaction {
|
||||
models.forEach { profile ->
|
||||
dao.insert(
|
||||
|
|
|
@ -27,6 +27,7 @@ import androidx.compose.foundation.shape.CornerSize
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
|
@ -707,3 +708,17 @@ fun AvatarBuilder(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AvatarBadgeIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
imageVector: ImageVector,
|
||||
) {
|
||||
Icon(
|
||||
modifier = modifier
|
||||
.size(16.dp)
|
||||
.padding(1.dp),
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -200,6 +200,8 @@
|
|||
<string name="account_action_change_master_password_hint_title">Change master password hint</string>
|
||||
<string name="account_action_sign_out_title">Sign out</string>
|
||||
<string name="account_action_sign_in_title">Sign in</string>
|
||||
<string name="account_action_hide_title">Hide items</string>
|
||||
<string name="account_action_hide_text">Hide the items from the main screens of the app</string>
|
||||
<string name="account_action_email_verify_instructions_title">Visit Web vault to verify your email address</string>
|
||||
<string name="account_action_premium_purchase_instructions_title">Upgrade your account to a premium membership and unlock some great additional features</string>
|
||||
<string name="account_action_tfa_title">Two-factor authentication</string>
|
||||
|
|
Loading…
Reference in New Issue