mirror of
https://github.com/bitwarden/android.git
synced 2026-06-09 08:09:16 -05:00
Update Vault Item Listing screen app bar for autofill (#834)
This commit is contained in:
committed by
Álison Fernandes
parent
0a6b0f8dc7
commit
5b854c17b7
@@ -10,7 +10,7 @@ import kotlinx.parcelize.Parcelize
|
||||
* @property uri A URI representing the location where data should be filled (if available).
|
||||
*/
|
||||
@Parcelize
|
||||
class AutofillSelectionData(
|
||||
data class AutofillSelectionData(
|
||||
val type: Type,
|
||||
val uri: String?,
|
||||
) : Parcelable {
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.core.graphics.toColorInt
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.text.Normalizer
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
@@ -54,6 +55,19 @@ fun String.isValidUri(): Boolean =
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the host name (or path as a fallback) for the given [String] if it represents a
|
||||
* well-formed URI, or `null` otherwise.
|
||||
*/
|
||||
fun String.toHostOrPathOrNull(): String? {
|
||||
val uri = try {
|
||||
URI(this)
|
||||
} catch (e: URISyntaxException) {
|
||||
return null
|
||||
}
|
||||
return uri.host ?: uri.path
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original [String] only if:
|
||||
*
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
@@ -92,6 +93,9 @@ fun BitwardenTopAppBar(
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
actions = actions,
|
||||
|
||||
@@ -67,6 +67,7 @@ class RootNavViewModel @Inject constructor(
|
||||
when (specialCircumstance) {
|
||||
is SpecialCircumstance.AutofillSelection -> {
|
||||
RootNavState.VaultUnlockedForAutofillSelection(
|
||||
activeUserId = userState.activeAccount.userId,
|
||||
type = specialCircumstance.autofillSelectionData.type,
|
||||
)
|
||||
}
|
||||
@@ -122,6 +123,7 @@ sealed class RootNavState : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data class VaultUnlockedForAutofillSelection(
|
||||
val activeUserId: String,
|
||||
val type: AutofillSelectionData.Type,
|
||||
) : RootNavState()
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -26,6 +29,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||
@@ -35,12 +40,15 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.NavigationIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Displays the vault item listing screen.
|
||||
@@ -50,7 +58,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
fun VaultItemListingScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVaultItem: (id: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (cipherVaultId: String) -> Unit,
|
||||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToAddSendItem: () -> Unit,
|
||||
onNavigateToEditSendItem: (sendId: String) -> Unit,
|
||||
@@ -158,6 +166,7 @@ private fun VaultItemListingScaffold(
|
||||
pullToRefreshState: PullToRefreshState?,
|
||||
vaultItemListingHandlers: VaultItemListingHandlers,
|
||||
) {
|
||||
var isAccountMenuVisible by rememberSaveable { mutableStateOf(false) }
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
@@ -165,28 +174,40 @@ private fun VaultItemListingScaffold(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = state.itemListingType.titleText(),
|
||||
title = state.appBarTitle(),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = painterResource(id = R.drawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
onNavigationIconClick = vaultItemListingHandlers.backClick,
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = painterResource(id = R.drawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
onNavigationIconClick = vaultItemListingHandlers.backClick,
|
||||
)
|
||||
.takeIf { state.shouldShowNavigationIcon },
|
||||
actions = {
|
||||
if (state.shouldShowAccountSwitcher) {
|
||||
BitwardenAccountActionItem(
|
||||
initials = state.activeAccountSummary.initials,
|
||||
color = state.activeAccountSummary.avatarColor,
|
||||
onClick = { isAccountMenuVisible = !isAccountMenuVisible },
|
||||
)
|
||||
}
|
||||
BitwardenSearchActionItem(
|
||||
contentDescription = stringResource(id = R.string.search_vault),
|
||||
onClick = vaultItemListingHandlers.searchIconClick,
|
||||
)
|
||||
BitwardenOverflowActionItem(
|
||||
menuItemDataList = persistentListOf(
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.sync),
|
||||
onClick = vaultItemListingHandlers.syncClick,
|
||||
if (state.shouldShowOverflowMenu) {
|
||||
BitwardenOverflowActionItem(
|
||||
menuItemDataList = persistentListOf(
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.sync),
|
||||
onClick = vaultItemListingHandlers.syncClick,
|
||||
),
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.lock),
|
||||
onClick = vaultItemListingHandlers.lockClick,
|
||||
),
|
||||
),
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.lock),
|
||||
onClick = vaultItemListingHandlers.lockClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
@@ -239,5 +260,20 @@ private fun VaultItemListingScaffold(
|
||||
BitwardenLoadingContent(modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
BitwardenAccountSwitcher(
|
||||
isVisible = isAccountMenuVisible,
|
||||
accountSummaries = state.accountSummaries.toImmutableList(),
|
||||
onSwitchAccountClick = vaultItemListingHandlers.switchAccountClick,
|
||||
onLockAccountClick = vaultItemListingHandlers.lockAccountClick,
|
||||
onLogoutAccountClick = vaultItemListingHandlers.logoutAccountClick,
|
||||
onAddAccountClick = {
|
||||
// Not available
|
||||
},
|
||||
onDismissRequest = { isAccountMenuVisible = false },
|
||||
isAddAccountAvailable = false,
|
||||
topAppBarScrollBehavior = scrollBehavior,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
@@ -22,6 +23,8 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
@@ -32,6 +35,8 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toSearchType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.updateWithAdditionalDataIfNecessary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -53,6 +58,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val clock: Clock,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
@@ -60,12 +66,17 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
|
||||
initialState = run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||
val accountSummaries = userState.toAccountSummaries()
|
||||
val specialCircumstance =
|
||||
specialCircumstanceManager.specialCircumstance as? SpecialCircumstance.AutofillSelection
|
||||
VaultItemListingState(
|
||||
itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle)
|
||||
.vaultItemListingType
|
||||
.toItemListingType(),
|
||||
activeAccountSummary = activeAccountSummary,
|
||||
accountSummaries = accountSummaries,
|
||||
viewState = VaultItemListingState.ViewState.Loading,
|
||||
vaultFilterType = vaultRepository.vaultFilterType,
|
||||
baseWebSendUrl = environmentRepository.environment.environmentUrlData.baseWebSendUrl,
|
||||
@@ -99,6 +110,9 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
|
||||
override fun handleAction(action: VaultItemListingsAction) {
|
||||
when (action) {
|
||||
is VaultItemListingsAction.LockAccountClick -> handleLockAccountClick(action)
|
||||
is VaultItemListingsAction.LogoutAccountClick -> handleLogoutAccountClick(action)
|
||||
is VaultItemListingsAction.SwitchAccountClick -> handleSwitchAccountClick(action)
|
||||
is VaultItemListingsAction.DismissDialogClick -> handleDismissDialogClick()
|
||||
is VaultItemListingsAction.BackClick -> handleBackClick()
|
||||
is VaultItemListingsAction.LockClick -> handleLockClick()
|
||||
@@ -114,6 +128,18 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
//region VaultItemListing Handlers
|
||||
private fun handleLockAccountClick(action: VaultItemListingsAction.LockAccountClick) {
|
||||
vaultRepository.lockVault(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleLogoutAccountClick(action: VaultItemListingsAction.LogoutAccountClick) {
|
||||
authRepository.logout(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleSwitchAccountClick(action: VaultItemListingsAction.SwitchAccountClick) {
|
||||
authRepository.switchAccount(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleRefreshClick() {
|
||||
vaultRepository.sync()
|
||||
}
|
||||
@@ -414,6 +440,12 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private fun handleVaultDataReceive(
|
||||
action: VaultItemListingsAction.Internal.VaultDataReceive,
|
||||
) {
|
||||
if (state.activeAccountSummary.userId != authRepository.userStateFlow.value?.activeUserId) {
|
||||
// We are in the process of switching accounts, so we should ignore any updates here
|
||||
// to avoid any unnecessary visual changes.
|
||||
return
|
||||
}
|
||||
|
||||
when (val vaultData = action.vaultData) {
|
||||
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
|
||||
is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
|
||||
@@ -541,6 +573,8 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
*/
|
||||
data class VaultItemListingState(
|
||||
val itemListingType: ItemListingType,
|
||||
val activeAccountSummary: AccountSummary,
|
||||
val accountSummaries: List<AccountSummary>,
|
||||
val viewState: ViewState,
|
||||
val vaultFilterType: VaultFilterType,
|
||||
val baseWebSendUrl: String,
|
||||
@@ -549,9 +583,24 @@ data class VaultItemListingState(
|
||||
val dialogState: DialogState?,
|
||||
// Internal
|
||||
private val isPullToRefreshSettingEnabled: Boolean,
|
||||
private val autofillSelectionData: AutofillSelectionData? = null,
|
||||
private val shouldFinishOnComplete: Boolean = false,
|
||||
val autofillSelectionData: AutofillSelectionData? = null,
|
||||
val shouldFinishOnComplete: Boolean = false,
|
||||
) {
|
||||
/**
|
||||
* Whether or not this represents a listing screen for autofill.
|
||||
*/
|
||||
val isAutofill: Boolean
|
||||
get() = autofillSelectionData != null
|
||||
|
||||
/**
|
||||
* A displayable title for the AppBar.
|
||||
*/
|
||||
val appBarTitle: Text
|
||||
get() = autofillSelectionData
|
||||
?.uri
|
||||
?.toHostOrPathOrNull()
|
||||
?.let { R.string.items_for_uri.asText(it) }
|
||||
?: itemListingType.titleText
|
||||
|
||||
/**
|
||||
* Indicates that the pull-to-refresh should be enabled in the UI.
|
||||
@@ -560,10 +609,19 @@ data class VaultItemListingState(
|
||||
get() = isPullToRefreshSettingEnabled && viewState.isPullToRefreshEnabled
|
||||
|
||||
/**
|
||||
* Whether or not this represents a listing screen for autofill.
|
||||
* Whether or not the account switcher should be shown.
|
||||
*/
|
||||
val isAutofill: Boolean
|
||||
get() = autofillSelectionData != null
|
||||
val shouldShowAccountSwitcher: Boolean get() = isAutofill
|
||||
|
||||
/**
|
||||
* Whether or not the navigation icon should be shown.
|
||||
*/
|
||||
val shouldShowNavigationIcon: Boolean get() = !isAutofill
|
||||
|
||||
/**
|
||||
* Whether or not the overflow menu should be shown.
|
||||
*/
|
||||
val shouldShowOverflowMenu: Boolean get() = !isAutofill
|
||||
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
@@ -843,6 +901,28 @@ sealed class VaultItemListingEvent {
|
||||
* Models actions for the [VaultItemListingScreen].
|
||||
*/
|
||||
sealed class VaultItemListingsAction {
|
||||
/**
|
||||
* Indicates the user has clicked on the given [accountSummary] information in order to lock
|
||||
* the associated account's vault.
|
||||
*/
|
||||
data class LockAccountClick(
|
||||
val accountSummary: AccountSummary,
|
||||
) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Indicates the user has clicked on the given [accountSummary] information in order to log out
|
||||
* of that account.
|
||||
*/
|
||||
data class LogoutAccountClick(
|
||||
val accountSummary: AccountSummary,
|
||||
) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* The user has clicked the an account to switch too.
|
||||
*/
|
||||
data class SwitchAccountClick(
|
||||
val accountSummary: AccountSummary,
|
||||
) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Click to dismiss the dialog.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingViewModel
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
@@ -9,6 +10,9 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflo
|
||||
* items.
|
||||
*/
|
||||
data class VaultItemListingHandlers(
|
||||
val switchAccountClick: (AccountSummary) -> Unit,
|
||||
val lockAccountClick: (AccountSummary) -> Unit,
|
||||
val logoutAccountClick: (AccountSummary) -> Unit,
|
||||
val backClick: () -> Unit,
|
||||
val searchIconClick: () -> Unit,
|
||||
val addVaultItemClick: () -> Unit,
|
||||
@@ -27,6 +31,15 @@ data class VaultItemListingHandlers(
|
||||
viewModel: VaultItemListingViewModel,
|
||||
): VaultItemListingHandlers =
|
||||
VaultItemListingHandlers(
|
||||
switchAccountClick = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.SwitchAccountClick(it))
|
||||
},
|
||||
lockAccountClick = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.LockAccountClick(it))
|
||||
},
|
||||
logoutAccountClick = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.LogoutAccountClick(it))
|
||||
},
|
||||
backClick = { viewModel.trySendAction(VaultItemListingsAction.BackClick) },
|
||||
searchIconClick = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.SearchIconClick)
|
||||
|
||||
Reference in New Issue
Block a user