Add send icons to row items. (#496)

This commit is contained in:
David Perez 2024-01-04 20:18:41 -06:00 committed by Álison Fernandes
parent 10bad26c95
commit 02c8f4bfec
12 changed files with 186 additions and 20 deletions

View file

@ -5,9 +5,11 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -23,22 +25,25 @@ import androidx.compose.ui.Modifier
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 androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* A Composable function that displays a row item.
*
* @param label The primary text label to display for the item.
* @param supportingLabel An secondary text label to display beneath the label.
* @param startIcon The [Painter] object used to draw the icon at the start of the item.
* @param onClick The lambda to be invoked when the item is clicked.
* @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier.
* This allows the caller to specify things like padding, size, etc.
* @param selectionDataList A list of all the selection items to be displayed in the overflow
* dialog.
* @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier.
* This allows the caller to specify things like padding, size, etc.
* @param supportingLabel An optional secondary text label to display beneath the label.
* @param trailingLabelIcons An optional list of small icons to be displayed after the [label].
*/
@Suppress("LongMethod")
@Composable
@ -49,6 +54,7 @@ fun BitwardenListItem(
selectionDataList: List<SelectionItemData>,
modifier: Modifier = Modifier,
supportingLabel: String? = null,
trailingLabelIcons: List<IconResource> = emptyList(),
) {
var shouldShowDialog by remember { mutableStateOf(false) }
Row(
@ -72,11 +78,28 @@ fun BitwardenListItem(
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(weight = 1f, fill = false),
)
trailingLabelIcons.forEach {
Spacer(modifier = Modifier.width(8.dp))
Icon(
painter = it.iconPainter,
contentDescription = it.contentDescription,
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier.size(16.dp),
)
}
}
supportingLabel?.let {
Text(

View file

@ -78,6 +78,7 @@ fun SendContent(
startIcon = painterResource(id = it.type.iconRes),
label = it.name,
supportingLabel = it.deletionDate,
trailingLabelIcons = it.iconList,
onClick = { sendHandlers.onSendClick(it) },
onCopyClick = { sendHandlers.onCopySendClick(it) },
onEditClick = { sendHandlers.onEditSendClick(it) },

View file

@ -9,7 +9,9 @@ import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon
/**
* A Composable function that displays a row send item.
@ -30,6 +32,7 @@ fun SendListItem(
label: String,
supportingLabel: String,
startIcon: Painter,
trailingLabelIcons: List<SendStatusIcon>,
onClick: () -> Unit,
onEditClick: () -> Unit,
onCopyClick: () -> Unit,
@ -40,6 +43,12 @@ fun SendListItem(
label = label,
supportingLabel = supportingLabel,
startIcon = startIcon,
trailingLabelIcons = trailingLabelIcons.map {
IconResource(
iconPainter = painterResource(it.iconRes),
contentDescription = it.contentDescription(),
)
},
onClick = onClick,
selectionDataList = listOf(
SelectionItemData(
@ -67,6 +76,7 @@ private fun SendListItem_preview() {
label = "Sample Label",
supportingLabel = "Jan 3, 2024, 10:35 AM",
startIcon = painterResource(id = R.drawable.ic_send_text),
trailingLabelIcons = emptyList(),
onClick = {},
onCopyClick = {},
onEditClick = {},

View file

@ -12,6 +12,7 @@ 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.tools.feature.send.model.SendStatusIcon
import com.x8bit.bitwarden.ui.tools.feature.send.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemScreen
import dagger.hilt.android.lifecycle.HiltViewModel
@ -201,6 +202,7 @@ data class SendState(
val name: String,
val deletionDate: String,
val type: Type,
val iconList: List<SendStatusIcon>,
) : Parcelable {
/**
* Indicates the type of send this, a text or file.

View file

@ -0,0 +1,31 @@
package com.x8bit.bitwarden.ui.tools.feature.send.model
import androidx.annotation.DrawableRes
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
/**
* Represents the types of icons to be displayed with the send.
*/
enum class SendStatusIcon(
@DrawableRes val iconRes: Int,
val contentDescription: Text,
) {
DISABLED(
iconRes = R.drawable.ic_send_disabled,
contentDescription = R.string.disabled.asText(),
),
PASSWORD(
iconRes = R.drawable.ic_send_password,
contentDescription = R.string.password.asText(),
),
EXPIRED(
iconRes = R.drawable.ic_send_expired,
contentDescription = R.string.expired.asText(),
),
MAX_ACCESS_COUNT_REACHED(
iconRes = R.drawable.ic_send_max_access_count_reached,
contentDescription = R.string.maximum_access_count_reached.asText(),
),
}

View file

@ -5,6 +5,8 @@ import com.bitwarden.core.SendView
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.ui.tools.feature.generator.util.toFormattedPattern
import com.x8bit.bitwarden.ui.tools.feature.send.SendState
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon
import java.time.Instant
private const val DELETION_DATE_PATTERN: String = "MMM d, uuuu, hh:mm a"
@ -22,16 +24,30 @@ private fun List<SendView>.toSendContent(): SendState.ViewState.Content {
return SendState.ViewState.Content(
textTypeCount = this.count { it.type == SendType.TEXT },
fileTypeCount = this.count { it.type == SendType.FILE },
sendItems = this.map {
SendState.ViewState.Content.SendItem(
id = requireNotNull(it.id),
name = it.name,
deletionDate = it.deletionDate.toFormattedPattern(DELETION_DATE_PATTERN),
type = when (it.type) {
SendType.TEXT -> SendState.ViewState.Content.SendItem.Type.TEXT
SendType.FILE -> SendState.ViewState.Content.SendItem.Type.FILE
},
)
},
sendItems = this
.map { sendView ->
SendState.ViewState.Content.SendItem(
id = requireNotNull(sendView.id),
name = sendView.name,
deletionDate = sendView.deletionDate.toFormattedPattern(DELETION_DATE_PATTERN),
type = when (sendView.type) {
SendType.TEXT -> SendState.ViewState.Content.SendItem.Type.TEXT
SendType.FILE -> SendState.ViewState.Content.SendItem.Type.FILE
},
iconList = listOfNotNull(
SendStatusIcon.EXPIRED.takeIf {
sendView.expirationDate?.isBefore(Instant.now()) == true
},
sendView.password?.let { SendStatusIcon.PASSWORD },
SendStatusIcon.MAX_ACCESS_COUNT_REACHED.takeIf {
sendView.maxAccessCount?.let { maxCount ->
sendView.accessCount >= maxCount
} == true
},
SendStatusIcon.DISABLED.takeIf { sendView.disabled },
),
)
}
.sortedBy { it.name },
)
}

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="16"
android:viewportWidth="16">
<group>
<clip-path android:pathData="M0,0h16v16h-16z" />
<path
android:fillColor="#505059"
android:fillType="evenOdd"
android:pathData="M9.222,1.66L15.774,11.817C16.441,12.85 15.587,14 14.553,14H1.447C0.412,14 -0.441,12.85 0.226,11.817L6.778,1.66C7.346,0.78 8.654,0.78 9.222,1.66ZM8.381,2.202C8.207,1.933 7.793,1.933 7.619,2.202L1.066,12.359C0.885,12.64 1.099,13 1.447,13H14.553C14.901,13 15.115,12.64 14.934,12.359L8.381,2.202Z" />
<path
android:fillColor="#505059"
android:fillType="evenOdd"
android:pathData="M8.001,5.3C8.277,5.3 8.501,5.523 8.501,5.8V8.917C8.501,9.193 8.277,9.417 8.001,9.417C7.724,9.417 7.501,9.193 7.501,8.917V5.8C7.501,5.523 7.724,5.3 8.001,5.3Z" />
<path
android:fillColor="#505059"
android:pathData="M8.676,11.471C8.676,11.844 8.373,12.146 8.001,12.146C7.628,12.146 7.326,11.844 7.326,11.471C7.326,11.098 7.628,10.796 8.001,10.796C8.373,10.796 8.676,11.098 8.676,11.471Z" />
</group>
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="16"
android:viewportWidth="16">
<group>
<clip-path android:pathData="M0,0h16v16h-16z" />
<path
android:fillColor="#505059"
android:fillType="evenOdd"
android:pathData="M8.5,4C8.776,4 9,4.224 9,4.5V8.586C9,8.984 8.842,9.365 8.561,9.647L6.854,11.354C6.658,11.549 6.342,11.549 6.146,11.354C5.951,11.158 5.951,10.842 6.146,10.647L7.854,8.939C7.947,8.846 8,8.719 8,8.586V4.5C8,4.224 8.224,4 8.5,4Z" />
<path
android:fillColor="#505059"
android:fillType="evenOdd"
android:pathData="M8,15C11.866,15 15,11.866 15,8C15,4.134 11.866,1 8,1C4.134,1 1,4.134 1,8C1,11.866 4.134,15 8,15ZM8,16C12.418,16 16,12.418 16,8C16,3.582 12.418,0 8,0C3.582,0 0,3.582 0,8C0,12.418 3.582,16 8,16Z" />
</group>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="20"
android:viewportWidth="20">
<group>
<clip-path android:pathData="M0,0h20v20h-20z" />
<path
android:fillColor="#000000"
android:pathData="M9.871,20C7.918,20 6.01,19.414 4.387,18.315C2.764,17.216 1.498,15.654 0.751,13.827C0.004,11.999 -0.191,9.989 0.19,8.049C0.571,6.11 1.511,4.327 2.891,2.929C4.272,1.53 6.031,0.579 7.945,0.192C9.859,-0.194 12.104,0.005 13.907,0.761C15.711,1.518 17.252,2.8 18.337,4.444C19.422,6.089 20,8.022 20,10C19.997,12.651 18.956,15.193 17.105,17.067C15.255,18.942 12.488,19.997 9.871,20ZM9.871,1.25C8.162,1.25 6.492,1.763 5.072,2.724C3.652,3.685 2.545,5.052 1.891,6.651C1.237,8.25 1.067,10.009 1.4,11.707C1.733,13.404 2.556,14.963 3.763,16.187C4.971,17.411 6.51,18.244 8.185,18.581C9.861,18.919 11.856,18.746 13.434,18.083C15.012,17.421 16.361,16.299 17.31,14.861C18.259,13.422 18.766,11.73 18.766,9.999C18.763,7.679 17.852,5.455 16.233,3.815C14.614,2.174 12.16,1.252 9.871,1.25Z" />
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M3.884,16.884L16.884,3.884L16,3L3,16L3.884,16.884Z" />
</group>
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="16"
android:viewportWidth="16">
<group>
<clip-path android:pathData="M0,0h16v16h-16z" />
<path
android:fillColor="#505059"
android:fillType="evenOdd"
android:pathData="M10.27,7.838L10.751,7.933C11.861,8.153 13.052,7.832 13.91,6.975C15.277,5.608 15.277,3.392 13.91,2.025C12.543,0.658 10.327,0.658 8.96,2.025C8.103,2.883 7.782,4.074 8.002,5.184L8.109,5.722L1.9,11.659L1.281,14.136H3.219L3.719,12.136H5.219L5.719,10.136H7.626L10.27,7.838ZM8,11.136H6.5L6,13.136H4.5L4,15.136H0L1,11.136L7.021,5.378C6.739,3.955 7.15,2.421 8.253,1.318C10.01,-0.439 12.86,-0.439 14.617,1.318C16.374,3.076 16.374,5.925 14.617,7.682C13.514,8.785 11.98,9.196 10.557,8.914L8,11.136Z" />
<path
android:fillColor="#505059"
android:fillType="evenOdd"
android:pathData="M11.789,3.44C11.593,3.635 11.593,3.951 11.789,4.147C11.984,4.342 12.3,4.342 12.496,4.147C12.691,3.951 12.691,3.635 12.496,3.44C12.3,3.244 11.984,3.244 11.789,3.44ZM13.203,2.732C13.789,3.318 13.789,4.268 13.203,4.854C12.617,5.44 11.667,5.44 11.082,4.854C10.496,4.268 10.496,3.318 11.082,2.732C11.667,2.147 12.617,2.147 13.203,2.732Z" />
</group>
</vector>

View file

@ -501,6 +501,7 @@ private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem =
name = "mockName-1",
deletionDate = "1",
type = SendState.ViewState.Content.SendItem.Type.FILE,
iconList = emptyList(),
)
private val DEFAULT_CONTENT_VIEW_STATE: SendState.ViewState.Content = SendState.ViewState.Content(
@ -513,6 +514,7 @@ private val DEFAULT_CONTENT_VIEW_STATE: SendState.ViewState.Content = SendState.
name = "mockName-2",
deletionDate = "1",
type = SendState.ViewState.Content.SendItem.Type.TEXT,
iconList = emptyList(),
),
),
)

View file

@ -4,6 +4,7 @@ import com.bitwarden.core.SendType
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.ui.tools.feature.send.SendState
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
@ -36,8 +37,8 @@ class SendDataExtensionsTest {
@Test
fun `toViewState should return Content when SendData is not empty`() {
val list = listOf(
createMockSendView(number = 1, type = SendType.FILE),
createMockSendView(number = 2, type = SendType.TEXT),
createMockSendView(number = 1, type = SendType.FILE),
)
val sendData = SendData(list)
@ -53,12 +54,22 @@ class SendDataExtensionsTest {
name = "mockName-1",
deletionDate = "Oct 27, 2023, 12:00 PM",
type = SendState.ViewState.Content.SendItem.Type.FILE,
iconList = listOf(
SendStatusIcon.EXPIRED,
SendStatusIcon.PASSWORD,
SendStatusIcon.MAX_ACCESS_COUNT_REACHED,
),
),
SendState.ViewState.Content.SendItem(
id = "mockId-2",
name = "mockName-2",
deletionDate = "Oct 27, 2023, 12:00 PM",
type = SendState.ViewState.Content.SendItem.Type.TEXT,
iconList = listOf(
SendStatusIcon.EXPIRED,
SendStatusIcon.PASSWORD,
SendStatusIcon.MAX_ACCESS_COUNT_REACHED,
),
),
),
),