[#1259] Add bubble message component

* [#1259] - Add bubble message component

- Closes #1418
- Wrap history and send message fields in bubble style
- Update changelog

* Change bubble message component file name

* Apply bubble design to SendConfirmationView

* Use different design for sent and received history transactions

* Simplify Send transaction recognition

* Color bubble arrow in transparent receiver case

---------

Co-authored-by: Serhii Ihnatiev <serhii.ihnatiev@ext.grandcentrix.net>
Co-authored-by: Honza Rychnovský <rychnovsky.honza@gmail.com>
This commit is contained in:
Serhii Ihnatiev 2024-06-14 08:54:20 +02:00 committed by GitHub
parent 34e741e3b3
commit 0a35c4fffd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 268 additions and 52 deletions

View File

@ -9,6 +9,10 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased]
### Added
- New bubble message style for the Send and Transaction history item text components
- Display all messages within the transaction history record when it is expanded
## [1.1.1 (660)] - 2024-06-05
### Added
@ -41,7 +45,6 @@ directly impact users rather than highlighting other key architectural updates.*
### Changed
- The app dialog window has now a bit more rounded corners
- A few more minor UI improvements
- Display all messages within transaction history record when it is expanded
## [1.0 (650)] - 2024-05-07

View File

@ -0,0 +1,136 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
private const val ARROW_PADDING = 16
private const val ARROW_WIDTH = 24
private const val ARROW_HEIGHT = 8
private const val COMPONENT_MIN_WIDTH = ARROW_WIDTH * 3
@Preview(showBackground = true)
@Composable
private fun BubbleWithTextPreview() {
ZcashTheme {
BubbleMessage(backgroundColor = ZcashTheme.colors.dividerColor) {
Text(
text = "TextTextTextText",
fontSize = 16.sp,
modifier = Modifier.padding(ZcashTheme.dimens.spacingDefault)
)
}
}
}
@Preview("Small content and left arrow preview", showBackground = true)
@Composable
private fun BubbleMinSizePreview() {
ZcashTheme {
BubbleMessage(arrowAlignment = BubbleArrowAlignment.BottomLeft) {
Text(
text = "T",
textAlign = TextAlign.Center,
fontSize = 16.sp,
modifier = Modifier.padding(ZcashTheme.dimens.spacingDefault)
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun BubbleWithTextFieldPreview() {
ZcashTheme {
Box(modifier = Modifier.fillMaxWidth()) {
BubbleMessage {
@OptIn(ExperimentalFoundationApi::class)
FormTextField(
value = "FormTextField",
onValueChange = {},
modifier = Modifier.padding(ZcashTheme.dimens.spacingDefault),
withBorder = false
)
}
}
}
}
@Composable
fun BubbleMessage(
modifier: Modifier = Modifier,
backgroundColor: Color = Color.Transparent,
borderStroke: BorderStroke = BorderStroke(1.dp, ZcashTheme.colors.textFieldFrame),
arrowAlignment: BubbleArrowAlignment = BubbleArrowAlignment.BottomRight,
content: @Composable () -> Unit
) {
val shape = createBubbleShape(arrowAlignment)
Surface(
modifier =
modifier
.clip(shape)
.border(shape = shape, border = borderStroke)
.background(backgroundColor)
.sizeIn(minWidth = COMPONENT_MIN_WIDTH.dp) // prevent collapsing when content is too small
.padding(bottom = ARROW_HEIGHT.dp), // compensate component height to center content
color = backgroundColor
) {
content()
}
}
@Composable
private fun createBubbleShape(arrowAlignment: BubbleArrowAlignment): Shape {
val density = LocalDensity.current
return GenericShape { size, _ ->
with(density) {
val arrowWidth = ARROW_WIDTH.dp.toPx()
val arrowHeight = ARROW_HEIGHT.dp.toPx()
val arrowPadding = ARROW_PADDING.dp.toPx()
moveTo(0f, 0f)
lineTo(size.width, 0f)
lineTo(size.width, size.height - arrowHeight)
when (arrowAlignment) {
BubbleArrowAlignment.BottomLeft -> {
lineTo(arrowWidth + arrowPadding, size.height - arrowHeight)
lineTo(arrowPadding, size.height)
lineTo(arrowPadding, size.height - arrowHeight)
}
BubbleArrowAlignment.BottomRight -> {
lineTo(size.width - arrowPadding, size.height - arrowHeight)
lineTo(size.width - arrowPadding, size.height)
lineTo(size.width - (arrowWidth + arrowPadding), size.height - arrowHeight)
}
}
lineTo(0f, size.height - arrowHeight)
close()
}
}
}
enum class BubbleArrowAlignment {
BottomLeft,
BottomRight
}

View File

@ -5,9 +5,7 @@ package co.electriccoin.zcash.ui.screen.account.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -39,6 +37,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient
@ -51,6 +50,8 @@ import co.electriccoin.zcash.ui.common.compose.SynchronizationStatus
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.component.BubbleArrowAlignment
import co.electriccoin.zcash.ui.design.component.BubbleMessage
import co.electriccoin.zcash.ui.design.component.CircularMidProgressIndicator
import co.electriccoin.zcash.ui.design.component.StyledBalance
import co.electriccoin.zcash.ui.design.component.TextWithIcon
@ -241,7 +242,34 @@ private fun ComposableHistoryListItemPreview() {
}
@Composable
@Preview("History List Item Expanded")
private fun ComposableHistoryListItemExpandedPreview() {
ZcashTheme(forceDarkMode = false) {
BlankSurface {
Column {
HistoryItem(
onAction = {},
transaction =
TransactionUiFixture.new(
overview = TransactionOverviewFixture.new().copy(isSentTransaction = true),
expandableState = TrxItemState.EXPANDED
)
)
HistoryItem(
onAction = {},
transaction =
TransactionUiFixture.new(
overview = TransactionOverviewFixture.new().copy(isSentTransaction = false),
expandableState = TrxItemState.EXPANDED
)
)
}
}
}
}
@Preview("Multiple History List Items")
@Composable
private fun ComposableHistoryListItemsPreview() {
ZcashTheme(forceDarkMode = false) {
BlankSurface {
@ -596,7 +624,6 @@ private fun HistoryItemExpandedPart(
state = transaction.overview.getExtendedState(),
onAction = onAction
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
} else if (transaction.recipientAddressType == null ||
@ -802,11 +829,17 @@ private fun HistoryItemMessagePart(
}
Column(modifier = modifier.then(Modifier.fillMaxWidth())) {
Box(
modifier =
Modifier
.fillMaxWidth()
.border(width = 1.dp, color = ZcashTheme.colors.textFieldFrame)
val (messageBackground, arrowAlignment) =
if (state.isSendType()) {
Color.Transparent to BubbleArrowAlignment.BottomLeft
} else {
ZcashTheme.colors.dividerColor to BubbleArrowAlignment.BottomRight
}
BubbleMessage(
modifier = Modifier.fillMaxWidth(),
backgroundColor = messageBackground,
arrowAlignment = arrowAlignment
) {
Text(
text = message,
@ -854,6 +887,8 @@ internal enum class TransactionExtendedState {
RECEIVE_FAILED;
fun isFailed(): Boolean = this == SEND_FAILED || this == RECEIVE_FAILED
fun isSendType(): Boolean = this == SENDING || this == SENT || this == SEND_FAILED
}
private fun TransactionOverview.getExtendedState(): TransactionExtendedState {

View File

@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext
@ -69,6 +70,8 @@ import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.BodySmall
import co.electriccoin.zcash.ui.design.component.BubbleArrowAlignment
import co.electriccoin.zcash.ui.design.component.BubbleMessage
import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Small
@ -110,6 +113,36 @@ private fun PreviewSendForm() {
}
}
@Composable
@Preview
private fun SendFormTransparentAddressPreview() {
ZcashTheme(forceDarkMode = false) {
Send(
sendStage = SendStage.Form,
onCreateZecSend = {},
focusManager = LocalFocusManager.current,
onBack = {},
onSettings = {},
onQrScannerOpen = {},
goBalances = {},
hasCameraFeature = true,
recipientAddressState =
RecipientAddressState(
address = "tmCxJG72RWN66xwPtNgu4iKHpyysGrc7rEg",
type = AddressType.Transparent
),
onRecipientAddressChange = {},
setAmountState = {},
amountState = AmountState.Valid(ZatoshiFixture.ZATOSHI_LONG.toString(), ZatoshiFixture.new()),
setMemoState = {},
memoState = MemoState.new("Test message"),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
walletSnapshot = WalletSnapshotFixture.new(),
balanceState = BalanceStateFixture.new()
)
}
}
// TODO [#1260]: Cover Send screens UI with tests
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
@ -618,8 +651,6 @@ fun SendFormAmountTextField(
}
}
// TODO [#1259]: Send.Form screen Memo field stroke bubble style
// TODO [#1259]: https://github.com/Electric-Coin-Company/zashi-android/issues/1259
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "LongParameterList")
@Composable
@ -670,45 +701,56 @@ fun SendFormMemoTextField(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
FormTextField(
enabled = isMemoFieldAvailable,
value =
BubbleMessage(
arrowAlignment = BubbleArrowAlignment.BottomLeft,
backgroundColor =
if (isMemoFieldAvailable) {
memoState.text
Color.Transparent
} else {
""
ZcashTheme.colors.textDisabled
}
) {
FormTextField(
enabled = isMemoFieldAvailable,
value =
if (isMemoFieldAvailable) {
memoState.text
} else {
""
},
onValueChange = {
setMemoState(MemoState.new(it))
},
onValueChange = {
setMemoState(MemoState.new(it))
},
bringIntoViewRequester = bringIntoViewRequester,
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done
),
keyboardActions =
KeyboardActions(
onDone = {
focusManager.clearFocus(true)
// Scroll down to make sure the Send button is visible on small screens
if (scrollTo > 0) {
scope.launch {
scrollState.animateScrollTo(scrollTo)
bringIntoViewRequester = bringIntoViewRequester,
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done
),
keyboardActions =
KeyboardActions(
onDone = {
focusManager.clearFocus(true)
// Scroll down to make sure the Send button is visible on small screens
if (scrollTo > 0) {
scope.launch {
scrollState.animateScrollTo(scrollTo)
}
}
}
}
),
placeholder = {
Text(
text = stringResource(id = R.string.send_memo_hint),
style = ZcashTheme.extendedTypography.textFieldHint,
color = ZcashTheme.colors.textFieldHint
)
},
modifier = Modifier.fillMaxWidth(),
minHeight = ZcashTheme.dimens.textFieldMemoPanelDefaultHeight,
)
),
placeholder = {
Text(
text = stringResource(id = R.string.send_memo_hint),
style = ZcashTheme.extendedTypography.textFieldHint,
color = ZcashTheme.colors.textFieldHint
)
},
modifier = Modifier.fillMaxWidth(),
minHeight = ZcashTheme.dimens.textFieldMemoPanelDefaultHeight,
withBorder = false
)
}
if (isMemoFieldAvailable) {
Body(

View File

@ -3,7 +3,6 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -25,6 +24,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
@ -32,7 +32,6 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
@ -48,6 +47,8 @@ import co.electriccoin.zcash.ui.design.component.AppAlertDialog
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.BubbleArrowAlignment
import co.electriccoin.zcash.ui.design.component.BubbleMessage
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Small
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
@ -284,11 +285,10 @@ private fun SendConfirmationContent(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
Box(
modifier =
Modifier
.fillMaxWidth()
.border(width = 1.dp, color = ZcashTheme.colors.textFieldFrame)
BubbleMessage(
modifier = Modifier.fillMaxWidth(),
arrowAlignment = BubbleArrowAlignment.BottomLeft,
backgroundColor = Color.Transparent
) {
Tiny(
text = zecSend.memo.value,