Add overflow items to sends listings (#665)

This commit is contained in:
David Perez
2024-01-18 16:45:09 -06:00
committed by Álison Fernandes
parent 8c64d6b01b
commit 413677852b
9 changed files with 516 additions and 21 deletions

View File

@@ -10,7 +10,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultEntryListItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
import kotlinx.collections.immutable.toPersistentList
/**
* Content view for the [VaultItemListingScreen].
@@ -19,6 +21,7 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.VaultEntryListItem
fun VaultItemListingContent(
state: VaultItemListingState.ViewState.Content,
vaultItemClick: (id: String) -> Unit,
onOverflowItemClick: (action: VaultItemListingsAction) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
@@ -34,11 +37,20 @@ fun VaultItemListingContent(
)
}
items(state.displayItemList) {
VaultEntryListItem(
BitwardenListItem(
startIcon = it.iconData,
label = it.title,
supportingLabel = it.subtitle,
onClick = { vaultItemClick(it.id) },
selectionDataList = it
.overflowOptions
.map { option ->
SelectionItemData(
text = option.title(),
onClick = { onOverflowItemClick(option.action) },
)
}
.toPersistentList(),
modifier = Modifier
.fillMaxWidth()
.padding(

View File

@@ -30,6 +30,8 @@ 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.OverflowMenuItemData
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 kotlinx.collections.immutable.persistentListOf
@@ -43,6 +45,7 @@ fun VaultItemListingScreen(
onNavigateToVaultAddItemScreen: () -> Unit,
onNavigateToAddSendItem: () -> Unit,
onNavigateToEditSendItem: (sendId: String) -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
viewModel: VaultItemListingViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsState()
@@ -56,6 +59,10 @@ fun VaultItemListingScreen(
onNavigateToVaultItem(event.id)
}
is VaultItemListingEvent.ShowShareSheet -> {
intentManager.shareText(event.content)
}
is VaultItemListingEvent.ShowToast -> {
Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show()
}
@@ -168,6 +175,7 @@ private fun VaultItemListingScaffold(
VaultItemListingContent(
state = state.viewState,
vaultItemClick = vaultItemListingHandlers.itemClick,
onOverflowItemClick = vaultItemListingHandlers.overflowItemClick,
modifier = modifier,
)
}

View File

@@ -4,10 +4,12 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
@@ -34,6 +36,7 @@ import javax.inject.Inject
@Suppress("MagicNumber", "TooManyFunctions")
class VaultItemListingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val clipboardManager: BitwardenClipboardManager,
private val vaultRepository: VaultRepository,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
@@ -43,6 +46,7 @@ class VaultItemListingViewModel @Inject constructor(
.vaultItemListingType
.toItemListingType(),
viewState = VaultItemListingState.ViewState.Loading,
baseWebSendUrl = environmentRepository.environment.environmentUrlData.baseWebSendUrl,
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
dialogState = null,
@@ -70,9 +74,17 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.ItemClick -> handleItemClick(action)
is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick()
is VaultItemListingsAction.RefreshClick -> handleRefreshClick()
is VaultItemListingsAction.CopySendUrlClick -> handleCopySendUrlClick(action)
is VaultItemListingsAction.DeleteSendClick -> handleDeleteSendClick(action)
is VaultItemListingsAction.ShareSendUrlClick -> handleShareSendUrlClick(action)
is VaultItemListingsAction.RemoveSendPasswordClick -> {
handleRemoveSendPasswordClick(action)
}
is VaultItemListingsAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
is VaultItemListingsAction.Internal.IconLoadingSettingReceive ->
is VaultItemListingsAction.Internal.IconLoadingSettingReceive -> {
handleIconsSettingReceived(action)
}
}
}
@@ -81,6 +93,26 @@ class VaultItemListingViewModel @Inject constructor(
vaultRepository.sync()
}
private fun handleCopySendUrlClick(action: VaultItemListingsAction.CopySendUrlClick) {
clipboardManager.setText(text = action.sendUrl)
}
private fun handleDeleteSendClick(action: VaultItemListingsAction.DeleteSendClick) {
// TODO: Implement deletion (BIT-1411)
sendEvent(VaultItemListingEvent.ShowToast("Not yet implemented".asText()))
}
private fun handleShareSendUrlClick(action: VaultItemListingsAction.ShareSendUrlClick) {
sendEvent(VaultItemListingEvent.ShowShareSheet(action.sendUrl))
}
private fun handleRemoveSendPasswordClick(
action: VaultItemListingsAction.RemoveSendPasswordClick,
) {
// TODO: Implement password removal (BIT-1411)
sendEvent(VaultItemListingEvent.ShowToast("Not yet implemented".asText()))
}
private fun handleAddVaultItemClick() {
val event = when (state.itemListingType) {
is VaultItemListingState.ItemListingType.Vault -> {
@@ -233,7 +265,7 @@ class VaultItemListingViewModel @Inject constructor(
.filter { sendView ->
sendView.determineListingPredicate(listingType)
}
.toViewState()
.toViewState(baseWebSendUrl = state.baseWebSendUrl)
}
},
dialogState = currentState.dialogState.takeUnless { clearDialogState },
@@ -248,6 +280,7 @@ class VaultItemListingViewModel @Inject constructor(
data class VaultItemListingState(
val itemListingType: ItemListingType,
val viewState: ViewState,
val baseWebSendUrl: String,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
val dialogState: DialogState?,
@@ -309,13 +342,26 @@ data class VaultItemListingState(
* @property title title of the item.
* @property subtitle subtitle of the item (nullable).
* @property iconData data for the icon to be displayed (nullable).
* @property overflowOptions list of options for the item's overflow menu.
*/
data class DisplayItem(
val id: String,
val title: String,
val subtitle: String?,
val iconData: IconData,
)
val overflowOptions: List<OverflowItem>,
) {
/**
* Represents a single option to be displayed in an [DisplayItem]s overflow menu.
*
* @property title the display title of the option.
* @property action the action to be sent back to the view model when the option is clicks.
*/
data class OverflowItem(
val title: Text,
val action: VaultItemListingsAction,
)
}
/**
* Represents different types of item listing.
@@ -470,6 +516,11 @@ sealed class VaultItemListingEvent {
*/
data object NavigateToVaultSearchScreen : VaultItemListingEvent()
/**
* Show a share sheet with the given content.
*/
data class ShowShareSheet(val content: String) : VaultItemListingEvent()
/**
* Show a toast with the given message.
*
@@ -520,6 +571,26 @@ sealed class VaultItemListingsAction {
*/
data class ItemClick(val id: String) : VaultItemListingsAction()
/**
* Click on the copy send URL overflow option.
*/
data class CopySendUrlClick(val sendUrl: String) : VaultItemListingsAction()
/**
* Click on the share send URL overflow option.
*/
data class ShareSendUrlClick(val sendUrl: String) : VaultItemListingsAction()
/**
* Click on the remove password send overflow option.
*/
data class RemoveSendPasswordClick(val sendId: String) : VaultItemListingsAction()
/**
* Click on the delete send overflow option.
*/
data class DeleteSendClick(val sendId: String) : VaultItemListingsAction()
/**
* Models actions that the [VaultItemListingViewModel] itself might send.
*/

View File

@@ -15,6 +15,7 @@ data class VaultItemListingHandlers(
val refreshClick: () -> Unit,
val syncClick: () -> Unit,
val lockClick: () -> Unit,
val overflowItemClick: (action: VaultItemListingsAction) -> Unit,
) {
companion object {
/**
@@ -36,6 +37,7 @@ data class VaultItemListingHandlers(
refreshClick = { viewModel.trySendAction(VaultItemListingsAction.RefreshClick) },
syncClick = { viewModel.trySendAction(VaultItemListingsAction.SyncClick) },
lockClick = { viewModel.trySendAction(VaultItemListingsAction.LockClick) },
overflowItemClick = { viewModel.trySendAction(it) },
)
}
}

View File

@@ -8,8 +8,11 @@ import com.bitwarden.core.FolderView
import com.bitwarden.core.SendType
import com.bitwarden.core.SendView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
/**
@@ -87,9 +90,13 @@ fun List<CipherView>.toViewState(
/**
* Transforms a list of [CipherView] into [VaultItemListingState.ViewState].
*/
fun List<SendView>.toViewState(): VaultItemListingState.ViewState =
fun List<SendView>.toViewState(
baseWebSendUrl: String,
): VaultItemListingState.ViewState =
if (isNotEmpty()) {
VaultItemListingState.ViewState.Content(displayItemList = toDisplayItemList())
VaultItemListingState.ViewState.Content(
displayItemList = toDisplayItemList(baseWebSendUrl = baseWebSendUrl),
)
} else {
VaultItemListingState.ViewState.NoItems
}
@@ -134,8 +141,10 @@ private fun List<CipherView>.toDisplayItemList(
)
}
private fun List<SendView>.toDisplayItemList(): List<VaultItemListingState.DisplayItem> =
this.map { it.toDisplayItem() }
private fun List<SendView>.toDisplayItemList(
baseWebSendUrl: String,
): List<VaultItemListingState.DisplayItem> =
this.map { it.toDisplayItem(baseWebSendUrl = baseWebSendUrl) }
private fun CipherView.toDisplayItem(
baseIconUrl: String,
@@ -149,6 +158,7 @@ private fun CipherView.toDisplayItem(
baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled,
),
overflowOptions = emptyList(),
)
private fun CipherView.toIconData(
@@ -169,7 +179,9 @@ private fun CipherView.toIconData(
}
}
private fun SendView.toDisplayItem(): VaultItemListingState.DisplayItem =
private fun SendView.toDisplayItem(
baseWebSendUrl: String,
): VaultItemListingState.DisplayItem =
VaultItemListingState.DisplayItem(
id = id.orEmpty(),
title = name,
@@ -180,6 +192,33 @@ private fun SendView.toDisplayItem(): VaultItemListingState.DisplayItem =
SendType.FILE -> R.drawable.ic_send_file
},
),
overflowOptions = listOfNotNull(
VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.edit.asText(),
action = VaultItemListingsAction.ItemClick(id = id.orEmpty()),
),
VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.copy_link.asText(),
action = VaultItemListingsAction.CopySendUrlClick(
sendUrl = toSendUrl(baseWebSendUrl),
),
),
VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.share_link.asText(),
action = VaultItemListingsAction.ShareSendUrlClick(
sendUrl = toSendUrl(baseWebSendUrl),
),
),
VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.remove_password.asText(),
action = VaultItemListingsAction.RemoveSendPasswordClick(sendId = id.orEmpty()),
)
.takeIf { hasPassword },
VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.delete.asText(),
action = VaultItemListingsAction.DeleteSendClick(sendId = id.orEmpty()),
),
),
)
@Suppress("MagicNumber")