Add trailing icons for sends (#675)

This commit is contained in:
David Perez 2024-01-18 21:00:06 -06:00 committed by Álison Fernandes
parent c7abbd17dc
commit a1e55297e9
8 changed files with 145 additions and 3 deletions

View file

@ -1,6 +1,10 @@
package com.x8bit.bitwarden.ui.platform.components.model package com.x8bit.bitwarden.ui.platform.components.model
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import com.x8bit.bitwarden.ui.platform.base.util.Text
/** /**
* Data class representing the resources required for an icon. * Data class representing the resources required for an icon.
@ -12,3 +16,31 @@ data class IconResource(
val iconPainter: Painter, val iconPainter: Painter,
val contentDescription: String, val contentDescription: String,
) )
/**
* Data class representing the resources required for an icon and is friendly to use in ViewModels.
*
* @property iconRes Resource for the icon.
* @property contentDescription The icon's content description.
*/
data class IconRes(
@DrawableRes
val iconRes: Int,
val contentDescription: Text,
)
/**
* A helper method to convert a list of [IconRes] to a list of [IconResource].
*/
@Composable
fun List<IconRes>.toIconResources(): List<IconResource> = this.map { it.toIconResource() }
/**
* A helper method to convert an [IconRes] to an [IconResource].
*/
@Composable
fun IconRes.toIconResource(): IconResource =
IconResource(
iconPainter = painterResource(id = iconRes),
contentDescription = contentDescription(),
)

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.toIconResources
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
/** /**
@ -42,6 +43,10 @@ fun VaultItemListingContent(
label = it.title, label = it.title,
supportingLabel = it.subtitle, supportingLabel = it.subtitle,
onClick = { vaultItemClick(it.id) }, onClick = { vaultItemClick(it.id) },
trailingLabelIcons = it
.extraIconList
.toIconResources()
.toPersistentList(),
selectionDataList = it selectionDataList = it
.overflowOptions .overflowOptions
.map { option -> .map { option ->

View file

@ -17,6 +17,7 @@ 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.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState
@ -28,6 +29,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Clock
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -38,6 +40,7 @@ import javax.inject.Inject
@Suppress("MagicNumber", "TooManyFunctions") @Suppress("MagicNumber", "TooManyFunctions")
class VaultItemListingViewModel @Inject constructor( class VaultItemListingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager, private val clipboardManager: BitwardenClipboardManager,
private val vaultRepository: VaultRepository, private val vaultRepository: VaultRepository,
private val environmentRepository: EnvironmentRepository, private val environmentRepository: EnvironmentRepository,
@ -271,7 +274,10 @@ class VaultItemListingViewModel @Inject constructor(
.filter { sendView -> .filter { sendView ->
sendView.determineListingPredicate(listingType) sendView.determineListingPredicate(listingType)
} }
.toViewState(baseWebSendUrl = state.baseWebSendUrl) .toViewState(
baseWebSendUrl = state.baseWebSendUrl,
clock = clock,
)
} }
}, },
dialogState = currentState.dialogState.takeUnless { clearDialogState }, dialogState = currentState.dialogState.takeUnless { clearDialogState },
@ -356,6 +362,7 @@ data class VaultItemListingState(
val title: String, val title: String,
val subtitle: String?, val subtitle: String?,
val iconData: IconData, val iconData: IconData,
val extraIconList: List<IconRes>,
val overflowOptions: List<OverflowItem>, val overflowOptions: List<OverflowItem>,
) { ) {
/** /**

View file

@ -10,10 +10,13 @@ import com.bitwarden.core.SendView
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl 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.VaultItemListingState
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import java.time.Clock
/** /**
* Determines a predicate to filter a list of [CipherView] based on the * Determines a predicate to filter a list of [CipherView] based on the
@ -92,10 +95,14 @@ fun List<CipherView>.toViewState(
*/ */
fun List<SendView>.toViewState( fun List<SendView>.toViewState(
baseWebSendUrl: String, baseWebSendUrl: String,
clock: Clock,
): VaultItemListingState.ViewState = ): VaultItemListingState.ViewState =
if (isNotEmpty()) { if (isNotEmpty()) {
VaultItemListingState.ViewState.Content( VaultItemListingState.ViewState.Content(
displayItemList = toDisplayItemList(baseWebSendUrl = baseWebSendUrl), displayItemList = toDisplayItemList(
baseWebSendUrl = baseWebSendUrl,
clock = clock,
),
) )
} else { } else {
VaultItemListingState.ViewState.NoItems VaultItemListingState.ViewState.NoItems
@ -143,8 +150,14 @@ private fun List<CipherView>.toDisplayItemList(
private fun List<SendView>.toDisplayItemList( private fun List<SendView>.toDisplayItemList(
baseWebSendUrl: String, baseWebSendUrl: String,
clock: Clock,
): List<VaultItemListingState.DisplayItem> = ): List<VaultItemListingState.DisplayItem> =
this.map { it.toDisplayItem(baseWebSendUrl = baseWebSendUrl) } this.map {
it.toDisplayItem(
baseWebSendUrl = baseWebSendUrl,
clock = clock,
)
}
private fun CipherView.toDisplayItem( private fun CipherView.toDisplayItem(
baseIconUrl: String, baseIconUrl: String,
@ -158,6 +171,7 @@ private fun CipherView.toDisplayItem(
baseIconUrl = baseIconUrl, baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled, isIconLoadingDisabled = isIconLoadingDisabled,
), ),
extraIconList = emptyList(),
overflowOptions = emptyList(), overflowOptions = emptyList(),
) )
@ -181,6 +195,7 @@ private fun CipherView.toIconData(
private fun SendView.toDisplayItem( private fun SendView.toDisplayItem(
baseWebSendUrl: String, baseWebSendUrl: String,
clock: Clock,
): VaultItemListingState.DisplayItem = ): VaultItemListingState.DisplayItem =
VaultItemListingState.DisplayItem( VaultItemListingState.DisplayItem(
id = id.orEmpty(), id = id.orEmpty(),
@ -192,6 +207,23 @@ private fun SendView.toDisplayItem(
SendType.FILE -> R.drawable.ic_send_file SendType.FILE -> R.drawable.ic_send_file
}, },
), ),
extraIconList = listOfNotNull(
SendStatusIcon.DISABLED
.takeIf { disabled }
?.let { IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription) },
SendStatusIcon.PASSWORD
.takeIf { hasPassword }
?.let { IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription) },
SendStatusIcon.MAX_ACCESS_COUNT_REACHED
.takeIf { maxAccessCount?.let { maxCount -> accessCount >= maxCount } == true }
?.let { IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription) },
SendStatusIcon.EXPIRED
.takeIf { expirationDate?.isBefore(clock.instant()) == true }
?.let { IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription) },
SendStatusIcon.PENDING_DELETE
.takeIf { deletionDate.isBefore(clock.instant()) }
?.let { IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription) },
),
overflowOptions = listOfNotNull( overflowOptions = listOfNotNull(
VaultItemListingState.DisplayItem.OverflowItem( VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.edit.asText(), title = R.string.edit.asText(),

View file

@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import com.x8bit.bitwarden.ui.util.isProgressBar import com.x8bit.bitwarden.ui.util.isProgressBar
@ -641,6 +642,28 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
title = "mockTitle-$number", title = "mockTitle-$number",
subtitle = "mockSubtitle-$number", subtitle = "mockSubtitle-$number",
iconData = IconData.Local(R.drawable.ic_card_item), iconData = IconData.Local(R.drawable.ic_card_item),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_send_disabled,
contentDescription = R.string.disabled.asText(),
),
IconRes(
iconRes = R.drawable.ic_send_password,
contentDescription = R.string.password.asText(),
),
IconRes(
iconRes = R.drawable.ic_send_max_access_count_reached,
contentDescription = R.string.maximum_access_count_reached.asText(),
),
IconRes(
iconRes = R.drawable.ic_send_expired,
contentDescription = R.string.expired.asText(),
),
IconRes(
iconRes = R.drawable.ic_send_pending_delete,
contentDescription = R.string.pending_delete.asText(),
),
),
overflowOptions = listOf( overflowOptions = listOf(
VaultItemListingState.DisplayItem.OverflowItem( VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.edit.asText(), title = R.string.edit.asText(),

View file

@ -36,9 +36,17 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class VaultItemListingViewModelTest : BaseViewModelTest() { class VaultItemListingViewModelTest : BaseViewModelTest() {
private val clock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val clipboardManager: BitwardenClipboardManager = mockk() private val clipboardManager: BitwardenClipboardManager = mockk()
private val mutableVaultDataStateFlow = private val mutableVaultDataStateFlow =
@ -636,6 +644,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
): VaultItemListingViewModel = ): VaultItemListingViewModel =
VaultItemListingViewModel( VaultItemListingViewModel(
savedStateHandle = savedStateHandle, savedStateHandle = savedStateHandle,
clock = clock,
clipboardManager = clipboardManager, clipboardManager = clipboardManager,
vaultRepository = vaultRepository, vaultRepository = vaultRepository,
environmentRepository = environmentRepository, environmentRepository = environmentRepository,

View file

@ -17,9 +17,17 @@ import io.mockk.mockkStatic
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class VaultItemListingDataExtensionsTest { class VaultItemListingDataExtensionsTest {
private val clock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
@Test @Test
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for non trash Login cipherView`() { fun `determineListingPredicate should return the correct predicate for non trash Login cipherView`() {
@ -361,6 +369,7 @@ class VaultItemListingDataExtensionsTest {
val result = sendViewList.toViewState( val result = sendViewList.toViewState(
baseWebSendUrl = Environment.Us.environmentUrlData.baseWebSendUrl, baseWebSendUrl = Environment.Us.environmentUrlData.baseWebSendUrl,
clock = clock,
) )
assertEquals( assertEquals(

View file

@ -5,6 +5,7 @@ import com.bitwarden.core.SendType
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState 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.itemlisting.VaultItemListingsAction
@ -25,6 +26,7 @@ fun createMockDisplayItemForCipher(
"https://vault.bitwarden.com/icons/www.mockuri.com/icon.png", "https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item, fallbackIconRes = R.drawable.ic_login_item,
), ),
extraIconList = emptyList(),
overflowOptions = emptyList(), overflowOptions = emptyList(),
) )
} }
@ -35,6 +37,7 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number", title = "mockName-$number",
subtitle = null, subtitle = null,
iconData = IconData.Local(R.drawable.ic_secure_note_item), iconData = IconData.Local(R.drawable.ic_secure_note_item),
extraIconList = emptyList(),
overflowOptions = emptyList(), overflowOptions = emptyList(),
) )
} }
@ -45,6 +48,7 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number", title = "mockName-$number",
subtitle = "er-$number", subtitle = "er-$number",
iconData = IconData.Local(R.drawable.ic_card_item), iconData = IconData.Local(R.drawable.ic_card_item),
extraIconList = emptyList(),
overflowOptions = emptyList(), overflowOptions = emptyList(),
) )
} }
@ -55,6 +59,7 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number", title = "mockName-$number",
subtitle = "mockFirstName-${number}mockLastName-$number", subtitle = "mockFirstName-${number}mockLastName-$number",
iconData = IconData.Local(R.drawable.ic_identity_item), iconData = IconData.Local(R.drawable.ic_identity_item),
extraIconList = emptyList(),
overflowOptions = emptyList(), overflowOptions = emptyList(),
) )
} }
@ -75,6 +80,16 @@ fun createMockDisplayItemForSend(
title = "mockName-$number", title = "mockName-$number",
subtitle = "2023-10-27T12:00:00Z", subtitle = "2023-10-27T12:00:00Z",
iconData = IconData.Local(R.drawable.ic_send_file), iconData = IconData.Local(R.drawable.ic_send_file),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_send_password,
contentDescription = R.string.password.asText(),
),
IconRes(
iconRes = R.drawable.ic_send_max_access_count_reached,
contentDescription = R.string.maximum_access_count_reached.asText(),
),
),
overflowOptions = listOfNotNull( overflowOptions = listOfNotNull(
VaultItemListingState.DisplayItem.OverflowItem( VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.edit.asText(), title = R.string.edit.asText(),
@ -112,6 +127,16 @@ fun createMockDisplayItemForSend(
title = "mockName-$number", title = "mockName-$number",
subtitle = "2023-10-27T12:00:00Z", subtitle = "2023-10-27T12:00:00Z",
iconData = IconData.Local(R.drawable.ic_send_text), iconData = IconData.Local(R.drawable.ic_send_text),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_send_password,
contentDescription = R.string.password.asText(),
),
IconRes(
iconRes = R.drawable.ic_send_max_access_count_reached,
contentDescription = R.string.maximum_access_count_reached.asText(),
),
),
overflowOptions = listOfNotNull( overflowOptions = listOfNotNull(
VaultItemListingState.DisplayItem.OverflowItem( VaultItemListingState.DisplayItem.OverflowItem(
title = R.string.edit.asText(), title = R.string.edit.asText(),