[#1162] Expandable transaction history item

Although this brings many changes to the transaction history item UI, it only partly solves Expandable transaction history item #1162
Follow-up PR will close it
This commit is contained in:
Honza Rychnovský 2024-03-29 20:27:30 +01:00 committed by GitHub
parent c99c2907b1
commit 4ae3fde690
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 704 additions and 309 deletions

View File

@ -14,6 +14,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AccountBox
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -30,6 +31,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.MonetarySeparators
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -227,16 +229,41 @@ fun Tiny(
}
@Composable
fun ListHeader(
@Suppress("LongParameterList")
fun TextWithIcon(
text: String,
modifier: Modifier = Modifier
imageVector: ImageVector,
modifier: Modifier = Modifier,
imageContentDescription: String? = null,
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,
textAlign: TextAlign = TextAlign.Start,
style: TextStyle = LocalTextStyle.current,
color: Color = MaterialTheme.colorScheme.onBackground,
) {
Text(
text = text,
style = ZcashTheme.extendedTypography.listItem,
color = ZcashTheme.colors.onBackgroundHeader,
modifier = modifier
)
Row(
modifier =
Modifier
.wrapContentSize()
.then(modifier),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = imageVector,
contentDescription = imageContentDescription
)
Spacer(modifier = Modifier.padding(3.dp))
Text(
text = text,
color = color,
maxLines = maxLines,
overflow = overflow,
textAlign = textAlign,
style = style,
)
}
}
@Suppress("LongParameterList")

View File

@ -47,6 +47,7 @@ data class Dimens(
val layoutStroke: Dp,
val regularRippleEffectCorner: Dp,
val smallRippleEffectCorner: Dp,
val tinyRippleEffectCorner: Dp,
// Screen custom spacings:
val inScreenZcashLogoHeight: Dp,
val inScreenZcashLogoWidth: Dp,
@ -87,6 +88,7 @@ private val defaultDimens =
divider = 1.dp,
regularRippleEffectCorner = 28.dp,
smallRippleEffectCorner = 10.dp,
tinyRippleEffectCorner = 8.dp,
inScreenZcashLogoHeight = 100.dp,
inScreenZcashLogoWidth = 60.dp,
inScreenZcashTextLogoHeight = 30.dp,

View File

@ -20,6 +20,7 @@ data class ExtendedColors(
val linearProgressBarBackground: Color,
val chipIndex: Color,
val textCommon: Color,
val textMedium: Color,
val textDisabled: Color,
val textFieldHint: Color,
val textFieldError: Color,

View File

@ -24,6 +24,7 @@ internal object Dark {
val textSecondaryButton = Color(0xFF000000)
val textTertiaryButton = Color.White
val textCommon = Color(0xFFFFFFFF)
val textMedium = Color(0xFF353535)
val textDisabled = Color(0xFFB7B7B7)
val textChipIndex = Color(0xFFFFB900)
val textFieldFrame = Color(0xFF231F20)
@ -43,18 +44,8 @@ internal object Dark {
val panelBackgroundColor = Color(0xFFEAEAEA)
val primaryButton = Color(0xFFFFFFFF)
val primaryButtonPressed = Color(0xFFFFFFFF)
val primaryButtonDisabled = Color(0xFFFFFFFF)
val secondaryButton = Color(0xFFFFFFFF)
val secondaryButtonPressed = Color(0xFFFFFFFF)
val secondaryButtonDisabled = Color(0xFFFFFFFF)
val tertiaryButton = Color.Transparent
val tertiaryButtonPressed = Color(0xFFFFFFFF)
val navigationButton = Color(0xFFFFFFFF)
val navigationButtonPressed = Color(0xFFFFFFFF)
val radioButtonColor = Color(0xFF070707)
val radioButtonTextColor = Color(0xFF4E4E4E)
@ -94,6 +85,7 @@ internal object Light {
val textSecondaryButton = Color(0xFF000000)
val textTertiaryButton = Color(0xFF000000)
val textCommon = Color(0xFF000000)
val textMedium = Color(0xFF353535)
val textDisabled = Color(0xFFB7B7B7)
val textFieldFrame = Color(0xFF231F20)
val textFieldError = Color(0xFFCD0002)
@ -113,18 +105,8 @@ internal object Light {
val panelBackgroundColor = Color(0xFFEAEAEA)
val primaryButton = Color(0xFF000000)
val primaryButtonPressed = Color(0xFF000000)
val primaryButtonDisabled = Color(0xFF000000)
val secondaryButton = Color(0xFFFFFFFF)
val secondaryButtonPressed = Color(0xFFFFFFFF)
val secondaryButtonDisabled = Color(0xFFFFFFFF)
val tertiaryButton = Color.Transparent
val tertiaryButtonPressed = Color(0xFFFFFFFF)
val navigationButton = Color(0xFFFFFFFF)
val navigationButtonPressed = Color(0xFFFFFFFF)
val radioButtonColor = Color(0xFF070707)
val radioButtonTextColor = Color(0xFF4E4E4E)
@ -191,6 +173,7 @@ internal val DarkExtendedColorPalette =
linearProgressBarBackground = Dark.linearProgressBarBackground,
chipIndex = Dark.textChipIndex,
textCommon = Dark.textCommon,
textMedium = Dark.textMedium,
textDisabled = Dark.textDisabled,
textFieldFrame = Dark.textFieldFrame,
textFieldError = Dark.textFieldError,
@ -234,6 +217,7 @@ internal val LightExtendedColorPalette =
linearProgressBarBackground = Light.linearProgressBarBackground,
chipIndex = Light.textChipIndex,
textCommon = Light.textCommon,
textMedium = Dark.textMedium,
textDisabled = Light.textDisabled,
textFieldFrame = Light.textFieldFrame,
textFieldError = Light.textFieldError,
@ -279,6 +263,7 @@ internal val LocalExtendedColors =
linearProgressBarBackground = Color.Unspecified,
chipIndex = Color.Unspecified,
textCommon = Color.Unspecified,
textMedium = Color.Unspecified,
textDisabled = Color.Unspecified,
textFieldHint = Color.Unspecified,
textFieldError = Color.Unspecified,

View File

@ -162,12 +162,13 @@ data class TransactionItemTextStyles(
val addressCollapsed: TextStyle,
val valueFirstPart: TextStyle,
val valueSecondPart: TextStyle,
val date: TextStyle,
val content: TextStyle,
val contentMedium: TextStyle,
val contentUnderline: TextStyle,
)
@Immutable
data class ExtendedTypography(
val listItem: TextStyle,
// Grouping balances text styles to a wrapper class for BalanceWidget
val balanceWidgetStyles: BalanceWidgetTextStyles,
// Grouping balances text styles to a wrapper class for single balance use case
@ -202,10 +203,6 @@ val LocalTypographies =
val LocalExtendedTypography =
staticCompositionLocalOf {
ExtendedTypography(
listItem =
PrimaryTypography.bodyLarge.copy(
fontSize = 24.sp
),
// Note: the order here matters, be careful when reordering
balanceWidgetStyles =
BalanceWidgetTextStyles(
@ -330,10 +327,21 @@ val LocalExtendedTypography =
PrimaryTypography.bodySmall.copy(
fontSize = 8.sp
),
date =
content =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp
),
contentMedium =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp,
fontWeight = FontWeight.Medium
),
contentUnderline =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp,
fontStyle = FontStyle.Italic,
textDecoration = TextDecoration.Underline
),
),
)
}

View File

@ -1,33 +1,12 @@
package co.electriccoin.zcash.ui.screen.account.history.fixture
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.TransactionRecipient
import co.electriccoin.zcash.ui.screen.account.model.HistoryItemExpandableState
import co.electriccoin.zcash.ui.screen.account.fixture.TransactionsFixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal object TransactionHistoryUiStateFixture {
val TRANSACTIONS =
persistentListOf(
TransactionUi(
TransactionOverviewFixture.new(),
TransactionRecipient.Account(Account.DEFAULT),
HistoryItemExpandableState.COLLAPSED
),
TransactionUi(
TransactionOverviewFixture.new(),
TransactionRecipient.Account(Account(1)),
HistoryItemExpandableState.EXPANDED
),
TransactionUi(
TransactionOverviewFixture.new(),
null,
HistoryItemExpandableState.COLLAPSED
),
)
val TRANSACTIONS = TransactionsFixture.new()
val STATE = TransactionUiState.Prepared(TRANSACTIONS)
fun new(

View File

@ -140,12 +140,10 @@ class HistoryViewTest {
assertEquals(TransactionHistorySyncStateFixture.TRANSACTIONS.size, testSetup.getOnItemIdClickCount())
}
private fun newTestSetup(
transactionHistorySyncState: TransactionUiState = TransactionHistoryUiStateFixture.new()
): HistoryTestSetup {
private fun newTestSetup(state: TransactionUiState = TransactionHistoryUiStateFixture.new()): HistoryTestSetup {
return HistoryTestSetup(
composeTestRule = composeTestRule,
initialHistoryUiState = transactionHistorySyncState
initialHistoryUiState = state
)
}
}

View File

@ -2,22 +2,27 @@
package co.electriccoin.zcash.ui.screen.account
import android.content.Context
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.Twig
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.account.view.Account
import co.electriccoin.zcash.ui.screen.account.view.TransactionItemAction
import co.electriccoin.zcash.ui.screen.account.view.TrxItemAction
import co.electriccoin.zcash.ui.screen.account.viewmodel.TransactionHistoryViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
@Composable
internal fun WrapAccount(
@ -39,15 +44,43 @@ internal fun WrapAccount(
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val transactionHistoryState = walletViewModel.transactionHistoryState.collectAsStateWithLifecycle().value
val transactionsUiState = transactionHistoryViewModel.transactionUiState.collectAsStateWithLifecycle().value
Twig.info { "Current transaction history state: $transactionsUiState" }
walletViewModel.transactionHistoryState.collectAsStateWithLifecycle().run {
transactionHistoryViewModel.processTransactionState(value)
}
transactionHistoryViewModel.processTransactionState(transactionHistoryState)
WrapAccount(
context = activity.applicationContext,
goBalances = goBalances,
goSettings = goSettings,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
scope = scope,
synchronizer = synchronizer,
transactionHistoryViewModel = transactionHistoryViewModel,
transactionsUiState = transactionsUiState,
walletSnapshot = walletSnapshot,
)
if (null == walletSnapshot) {
// For benchmarking purposes
activity.reportFullyDrawn()
}
@Composable
@VisibleForTesting
@Suppress("LongParameterList")
internal fun WrapAccount(
context: Context,
scope: CoroutineScope,
goBalances: () -> Unit,
goSettings: () -> Unit,
transactionsUiState: TransactionUiState,
synchronizer: Synchronizer?,
transactionHistoryViewModel: TransactionHistoryViewModel,
walletSnapshot: WalletSnapshot?,
isKeepScreenOnWhileSyncing: Boolean?,
) {
if (null == synchronizer || null == walletSnapshot) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
@ -59,39 +92,44 @@ internal fun WrapAccount(
transactionsUiState = transactionsUiState,
onTransactionItemAction = { action ->
when (action) {
is TransactionItemAction.IdClick -> {
Twig.info { "Transaction ID clicked: ${action.id}" }
is TrxItemAction.TransactionIdClick -> {
Twig.info { "Transaction ID clicked" }
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.account_history_id_clipboard_tag),
context,
context.getString(R.string.account_history_id_clipboard_tag),
action.id
)
}
is TransactionItemAction.MemoClick -> {
Twig.info { "Transaction item clicked - querying memos..." }
val memos = synchronizer?.getMemos(action.overview)
is TrxItemAction.ExpandableStateChange -> {
Twig.info { "Transaction new state: ${action.newState.name}" }
scope.launch {
memos?.toList()?.let {
val merged = it.joinToString().ifEmpty { "-" }
Twig.info { "Transaction memos: count: ${it.size}, contains: $merged" }
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.account_history_item_clipboard_tag),
merged
)
}
transactionHistoryViewModel.updateTransactionItemState(
synchronizer = synchronizer,
txId = action.txId,
newState = action.newState
)
}
}
is TransactionItemAction.ExpandableStateChange -> {
transactionHistoryViewModel.updateTransactionItemState(action.txId, action.newState)
is TrxItemAction.AddressClick -> {
Twig.info { "Transaction address clicked" }
ClipboardManagerUtil.copyToClipboard(
context.applicationContext,
context.getString(R.string.account_history_address_clipboard_tag),
action.address.addressValue
)
}
is TrxItemAction.MessageClick -> {
Twig.info { "Transaction message clicked" }
ClipboardManagerUtil.copyToClipboard(
context.applicationContext,
context.getString(R.string.account_history_memo_clipboard_tag),
action.memo
)
}
}
},
goBalances = goBalances,
goSettings = goSettings,
)
// For benchmarking purposes
activity.reportFullyDrawn()
}
}

View File

@ -0,0 +1,34 @@
package co.electriccoin.zcash.ui.screen.account.fixture
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import cash.z.ecc.android.sdk.fixture.WalletFixture
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.ZcashNetwork
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.model.TrxItemState
object TransactionUiFixture {
val OVERVIEW: TransactionOverview = TransactionOverviewFixture.new()
val RECIPIENT: TransactionRecipient =
TransactionRecipient.Address(
WalletFixture.Alice.getAddresses(ZcashNetwork.Mainnet).sapling
)
val EXPANDABLE_STATE: TrxItemState = TrxItemState.COLLAPSED
val MESSAGES: List<String> = listOf("Thanks for the coffee", "It was great to meet you!")
internal fun new(
overview: TransactionOverview = OVERVIEW,
recipient: TransactionRecipient = RECIPIENT,
expandableState: TrxItemState = EXPANDABLE_STATE,
messages: List<String> = MESSAGES,
) = TransactionUi(
overview = overview,
recipient = recipient,
expandableState = expandableState,
messages = messages
)
}

View File

@ -0,0 +1,17 @@
package co.electriccoin.zcash.ui.screen.account.fixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
object TransactionsFixture {
val TRANSACTIONS: ImmutableList<TransactionUi> =
persistentListOf(
TransactionUiFixture.new(),
TransactionUiFixture.new(),
TransactionUiFixture.new(),
TransactionUiFixture.new()
)
internal fun new() = TRANSACTIONS
}

View File

@ -7,16 +7,19 @@ import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
data class TransactionUi(
val overview: TransactionOverview,
val recipient: TransactionRecipient?,
val expandableState: HistoryItemExpandableState
val expandableState: TrxItemState,
val messages: List<String>?
) {
companion object {
fun new(
data: TransactionOverviewExt,
expandableState: HistoryItemExpandableState
expandableState: TrxItemState,
messages: List<String>?
) = TransactionUi(
overview = data.overview,
recipient = data.recipient,
expandableState = expandableState
expandableState = expandableState,
messages = messages
)
}
}

View File

@ -1,8 +1,9 @@
package co.electriccoin.zcash.ui.screen.account.model
enum class HistoryItemExpandableState {
enum class TrxItemState {
COLLAPSED,
EXPANDED,
EXPANDED_ADDRESS,
EXPANDED_ID
EXPANDED_ID,
EXPANDED_ALL,
}

View File

@ -16,8 +16,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceWidget
import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout
@ -28,10 +26,8 @@ import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.model.HistoryItemExpandableState
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.fixture.TransactionsFixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import kotlinx.collections.immutable.persistentListOf
@Preview("Account No History")
@Composable
@ -61,27 +57,7 @@ private fun HistoryListComposablePreview() {
isKeepScreenOnWhileSyncing = false,
goBalances = {},
goSettings = {},
transactionsUiState =
TransactionUiState.Prepared(
transactions =
persistentListOf(
TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
null,
HistoryItemExpandableState.EXPANDED
),
TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)),
null,
HistoryItemExpandableState.COLLAPSED
),
TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)),
null,
HistoryItemExpandableState.COLLAPSED
),
)
),
transactionsUiState = TransactionUiState.Prepared(transactions = TransactionsFixture.new()),
onTransactionItemAction = {},
)
}
@ -94,7 +70,7 @@ internal fun Account(
goBalances: () -> Unit,
goSettings: () -> Unit,
isKeepScreenOnWhileSyncing: Boolean?,
onTransactionItemAction: (TransactionItemAction) -> Unit,
onTransactionItemAction: (TrxItemAction) -> Unit,
transactionsUiState: TransactionUiState,
walletSnapshot: WalletSnapshot,
) {
@ -141,7 +117,7 @@ private fun AccountMainContent(
walletSnapshot: WalletSnapshot,
isKeepScreenOnWhileSyncing: Boolean?,
goBalances: () -> Unit,
onTransactionItemAction: (TransactionItemAction) -> Unit,
onTransactionItemAction: (TrxItemAction) -> Unit,
transactionState: TransactionUiState,
modifier: Modifier = Modifier
) {

View File

@ -1,19 +1,24 @@
@file:Suppress("TooManyFunctions")
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
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Divider
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.Icon
@ -22,6 +27,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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
@ -31,25 +37,26 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.TransactionState
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.toZecString
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.CircularMidProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.StyledBalance
import co.electriccoin.zcash.ui.design.component.Tiny
import co.electriccoin.zcash.ui.design.component.TextWithIcon
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.HistoryTag
import co.electriccoin.zcash.ui.screen.account.model.HistoryItemExpandableState
import co.electriccoin.zcash.ui.screen.account.fixture.TransactionUiFixture
import co.electriccoin.zcash.ui.screen.account.fixture.TransactionsFixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.account.model.TrxItemState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
@ -72,41 +79,19 @@ private fun ComposablePreview() {
private fun ComposableHistoryListPreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
@Suppress("MagicNumber")
HistoryContainer(
transactionState =
TransactionUiState.Prepared(
transactions =
persistentListOf(
TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
null,
HistoryItemExpandableState.EXPANDED
),
TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)),
null,
HistoryItemExpandableState.COLLAPSED
),
TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)),
null,
HistoryItemExpandableState.COLLAPSED
),
)
),
transactionState = TransactionUiState.Prepared(transactions = TransactionsFixture.new()),
onTransactionItemAction = {}
)
}
}
}
val dateFormat: DateFormat by lazy {
SimpleDateFormat.getDateTimeInstance(
SimpleDateFormat.MEDIUM,
SimpleDateFormat.SHORT,
// TODO [#1171]: Remove default MonetarySeparators locale
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
// TODO [#1171]: Remove default MonetarySeparators locale
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
private val dateFormat: DateFormat by lazy {
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.US
)
}
@ -114,7 +99,7 @@ val dateFormat: DateFormat by lazy {
@Composable
internal fun HistoryContainer(
transactionState: TransactionUiState,
onTransactionItemAction: (TransactionItemAction) -> Unit,
onTransactionItemAction: (TrxItemAction) -> Unit,
modifier: Modifier = Modifier
) {
Box(
@ -166,10 +151,11 @@ internal fun HistoryContainer(
@Composable
private fun HistoryList(
transactions: ImmutableList<TransactionUi>,
onAction: (TransactionItemAction) -> Unit
onAction: (TrxItemAction) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = Modifier.testTag(HistoryTag.TRANSACTION_LIST)
modifier = modifier.then(Modifier.testTag(HistoryTag.TRANSACTION_LIST))
) {
items(transactions.size) { index ->
HistoryItem(
@ -191,15 +177,9 @@ private fun HistoryList(
private fun ComposableHistoryListItemPreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
@Suppress("MagicNumber")
HistoryItem(
onAction = {},
transaction =
TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
recipient = null,
expandableState = HistoryItemExpandableState.EXPANDED
)
transaction = TransactionUiFixture.new()
)
}
}
@ -208,10 +188,10 @@ private fun ComposableHistoryListItemPreview() {
const val ADDRESS_IN_TITLE_WIDTH_RATIO = 0.5f
@Composable
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Suppress("LongMethod")
private fun HistoryItem(
transaction: TransactionUi,
onAction: (TransactionItemAction) -> Unit,
onAction: (TrxItemAction) -> Unit,
modifier: Modifier = Modifier
) {
val typeText: String
@ -221,38 +201,38 @@ private fun HistoryItem(
when (transaction.overview.getExtendedState()) {
TransactionExtendedState.SENT -> {
typeText = stringResource(id = R.string.account_history_item_sent)
typeIcon = ImageVector.vectorResource(R.drawable.trx_send_icon)
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_send_icon)
textColor = MaterialTheme.colorScheme.onBackground
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular
}
TransactionExtendedState.SENDING -> {
typeText = stringResource(id = R.string.account_history_item_sending)
typeIcon = ImageVector.vectorResource(R.drawable.trx_send_icon)
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_send_icon)
textColor = ZcashTheme.colors.textDescription
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRunning
}
TransactionExtendedState.SEND_FAILED -> {
typeText = stringResource(id = R.string.account_history_item_send_failed)
typeIcon = ImageVector.vectorResource(R.drawable.trx_send_icon)
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_send_icon)
textColor = ZcashTheme.colors.dangerous
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleFailed
}
TransactionExtendedState.RECEIVED -> {
typeText = stringResource(id = R.string.account_history_item_received)
typeIcon = ImageVector.vectorResource(R.drawable.trx_receive_icon)
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_receive_icon)
textColor = MaterialTheme.colorScheme.onBackground
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular
}
TransactionExtendedState.RECEIVING -> {
typeText = stringResource(id = R.string.account_history_item_receiving)
typeIcon = ImageVector.vectorResource(R.drawable.trx_receive_icon)
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_receive_icon)
textColor = ZcashTheme.colors.textDescription
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRunning
}
TransactionExtendedState.RECEIVE_FAILED -> {
typeText = stringResource(id = R.string.account_history_item_receive_failed)
typeIcon = ImageVector.vectorResource(R.drawable.trx_receive_icon)
typeIcon = ImageVector.vectorResource(R.drawable.ic_trx_receive_icon)
textColor = ZcashTheme.colors.dangerous
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleFailed
}
@ -260,161 +240,437 @@ private fun HistoryItem(
Row(
modifier =
modifier
.then(
Modifier
.background(color = ZcashTheme.colors.historyBackgroundColor)
.clickable {
if (transaction.expandableState <= HistoryItemExpandableState.COLLAPSED) {
onAction(
TransactionItemAction.ExpandableStateChange(
transaction.overview.rawId,
HistoryItemExpandableState.EXPANDED
)
modifier.then(
Modifier
.background(color = ZcashTheme.colors.historyBackgroundColor)
.clickable {
if (transaction.expandableState <= TrxItemState.COLLAPSED) {
onAction(
TrxItemAction.ExpandableStateChange(
transaction.overview.rawId,
TrxItemState.EXPANDED
)
}
)
}
.padding(all = ZcashTheme.dimens.spacingLarge)
.animateContentSize()
)
}
.padding(all = ZcashTheme.dimens.spacingLarge)
.animateContentSize()
)
) {
Image(
imageVector = typeIcon,
contentDescription = typeText,
contentDescription = typeText
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingDefault))
Column {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = typeText,
style = textStyle,
color = textColor,
modifier = Modifier.testTag(HistoryTag.TRANSACTION_ITEM_TITLE)
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
if (transaction.recipient != null && transaction.recipient is TransactionRecipient.Address) {
Text(
text = transaction.recipient.addressValue,
style = ZcashTheme.extendedTypography.transactionItemStyles.addressCollapsed,
color = ZcashTheme.colors.textDescription,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.fillMaxWidth(ADDRESS_IN_TITLE_WIDTH_RATIO)
)
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.trx_shielded),
contentDescription = stringResource(id = R.string.account_history_item_shielded)
)
}
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
Spacer(modifier = Modifier.weight(1f))
StyledBalance(
balanceString = transaction.overview.netValue.toZecString(),
textStyles =
Pair(
first = ZcashTheme.extendedTypography.transactionItemStyles.valueFirstPart,
second = ZcashTheme.extendedTypography.transactionItemStyles.valueSecondPart
),
textColor =
if (transaction.overview.isSentTransaction) {
ZcashTheme.colors.historySendColor
} else {
ZcashTheme.colors.textCommon
},
prefix =
if (transaction.overview.isSentTransaction) {
stringResource(id = R.string.account_history_item_sent_prefix)
} else {
stringResource(id = R.string.account_history_item_received_prefix)
}
)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
val dateString =
transaction.overview.minedHeight?.let {
transaction.overview.blockTimeEpochSeconds?.let { blockTimeEpochSeconds ->
// * 1000 to covert to millis
@Suppress("MagicNumber")
dateFormat.format(blockTimeEpochSeconds.times(1000))
} ?: ""
} ?: ""
Text(
text = dateString,
style = ZcashTheme.extendedTypography.transactionItemStyles.date,
color = ZcashTheme.colors.textDescription,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
HistoryItemCollapsedMainPart(
transaction = transaction,
typeText = typeText,
textStyle = textStyle,
textColor = textColor,
onAction = onAction
)
if (transaction.expandableState == HistoryItemExpandableState.EXPANDED) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXtiny))
val txId = transaction.overview.txIdString()
Tiny(
text = txId,
modifier =
Modifier
.clickable { onAction(TransactionItemAction.IdClick(txId)) }
.testTag(HistoryTag.TRANSACTION_ID)
)
// To add an extra spacing at the end
Column(
modifier = Modifier.padding(end = ZcashTheme.dimens.spacingUpLarge)
) {
val isInExpectedState =
transaction.expandableState == TrxItemState.EXPANDED_ADDRESS ||
transaction.expandableState == TrxItemState.EXPANDED_ALL
Spacer(modifier = (Modifier.height(ZcashTheme.dimens.spacingDefault)))
if (isInExpectedState &&
transaction.recipient != null &&
transaction.recipient is TransactionRecipient.Address
) {
HistoryItemExpandedAddressPart(onAction, transaction.recipient)
// TODO [#1162]: Will be reworked
// TODO [#1162]: Expandable transaction history item
// TODO [#1162]: https://github.com/Electric-Coin-Company/zashi-android/issues/1162
Tiny(
text = "Tap to copy message",
modifier = Modifier.clickable { onAction(TransactionItemAction.MemoClick(transaction.overview)) }
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
Spacer(modifier = (Modifier.height(ZcashTheme.dimens.spacingDefault)))
HistoryItemDatePart(transaction)
Tiny(
text = stringResource(id = R.string.account_history_item_collapse_transaction),
modifier =
Modifier
.clickable {
if (transaction.expandableState >= HistoryItemExpandableState.EXPANDED) {
if (transaction.expandableState >= TrxItemState.EXPANDED) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
HistoryItemExpandedPart(onAction, transaction)
}
}
}
}
}
@Composable
@Suppress("LongParameterList")
private fun HistoryItemCollapsedMainPart(
transaction: TransactionUi,
typeText: String,
textStyle: TextStyle,
textColor: Color,
onAction: (TrxItemAction) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier =
modifier.then(
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 24.dp)
),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = typeText,
style = textStyle,
color = textColor,
modifier = Modifier.testTag(HistoryTag.TRANSACTION_ITEM_TITLE)
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny))
HistoryItemCollapsedAddressPart(onAction, transaction)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
Spacer(modifier = Modifier.weight(1f))
StyledBalance(
balanceString = transaction.overview.netValue.toZecString(),
textStyles =
Pair(
first = ZcashTheme.extendedTypography.transactionItemStyles.valueFirstPart,
second = ZcashTheme.extendedTypography.transactionItemStyles.valueSecondPart
),
textColor =
if (transaction.overview.isSentTransaction) {
ZcashTheme.colors.historySendColor
} else {
ZcashTheme.colors.textCommon
},
prefix =
if (transaction.overview.isSentTransaction) {
stringResource(id = R.string.account_history_item_sent_prefix)
} else {
stringResource(id = R.string.account_history_item_received_prefix)
}
)
}
}
@Composable
private fun HistoryItemCollapsedAddressPart(
onAction: (TrxItemAction) -> Unit,
transaction: TransactionUi,
modifier: Modifier = Modifier
) {
if (transaction.recipient != null && transaction.recipient is TransactionRecipient.Address) {
when (transaction.expandableState) {
TrxItemState.EXPANDED_ADDRESS, TrxItemState.EXPANDED_ALL -> {
// No address displayed in the top row
}
else -> {
val clickModifier =
modifier.then(
if (transaction.expandableState <= TrxItemState.COLLAPSED) {
Modifier.padding(all = ZcashTheme.dimens.spacingXtiny)
} else {
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable {
onAction(
TransactionItemAction.ExpandableStateChange(
TrxItemAction.ExpandableStateChange(
transaction.overview.rawId,
HistoryItemExpandableState.COLLAPSED
if (transaction.expandableState == TrxItemState.EXPANDED_ID) {
TrxItemState.EXPANDED_ALL
} else {
TrxItemState.EXPANDED_ADDRESS
}
)
)
}
}
.padding(all = ZcashTheme.dimens.spacingXtiny)
}
)
Text(
text = transaction.recipient.addressValue,
style = ZcashTheme.extendedTypography.transactionItemStyles.addressCollapsed,
color = ZcashTheme.colors.textDescription,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.fillMaxWidth(ADDRESS_IN_TITLE_WIDTH_RATIO)
.then(clickModifier)
)
}
}
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_trx_shielded),
contentDescription = stringResource(id = R.string.account_history_item_shielded)
)
}
}
const val EXPANDED_ADDRESS_WIDTH_RATIO = 0.75f
@Composable
private fun HistoryItemExpandedAddressPart(
onAction: (TrxItemAction) -> Unit,
recipient: TransactionRecipient.Address,
modifier: Modifier = Modifier
) {
Column(
modifier =
modifier.then(
Modifier.fillMaxWidth()
)
) {
Text(
text = recipient.addressValue,
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textCommon,
modifier =
Modifier
.fillMaxWidth(EXPANDED_ADDRESS_WIDTH_RATIO)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
TextWithIcon(
text = stringResource(id = R.string.account_history_item_tap_to_copy),
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textDescription,
imageVector = ImageVector.vectorResource(R.drawable.ic_trx_copy),
modifier =
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable { onAction(TrxItemAction.AddressClick(recipient)) }
.padding(all = ZcashTheme.dimens.spacingTiny)
)
}
}
@Composable
private fun HistoryItemDatePart(
transaction: TransactionUi,
modifier: Modifier = Modifier
) {
val formattedDate =
transaction.overview.blockTimeEpochSeconds?.let { blockTimeEpochSeconds ->
// * 1000 to covert to millis
@Suppress("MagicNumber")
dateFormat.format(blockTimeEpochSeconds.times(1000))
}
if (formattedDate != null) {
Text(
text = formattedDate,
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textDescription,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
)
}
}
@Composable
private fun HistoryItemExpandedPart(
onAction: (TrxItemAction) -> Unit,
transaction: TransactionUi,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
if (!transaction.messages.isNullOrEmpty()) {
HistoryItemMessagePart(transaction.messages.toPersistentList(), onAction)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
HistoryItemTransactionIdPart(
transaction = transaction,
onAction = onAction
)
Spacer(modifier = (Modifier.height(ZcashTheme.dimens.spacingDefault)))
TextWithIcon(
text = stringResource(id = R.string.account_history_item_collapse_transaction),
style = ZcashTheme.extendedTypography.transactionItemStyles.contentUnderline,
color = ZcashTheme.colors.textDescription,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_trx_collapse),
modifier =
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable {
if (transaction.expandableState >= TrxItemState.EXPANDED) {
onAction(
TrxItemAction.ExpandableStateChange(
transaction.overview.rawId,
TrxItemState.COLLAPSED
)
)
}
}
.padding(all = ZcashTheme.dimens.spacingTiny)
)
}
}
const val EXPANDED_TRANSACTION_ID_WIDTH_RATIO = 0.75f
const val COLLAPSED_TRANSACTION_ID_WIDTH_RATIO = 0.5f
@Composable
@Suppress("LongMethod")
private fun HistoryItemTransactionIdPart(
transaction: TransactionUi,
onAction: (TrxItemAction) -> Unit,
modifier: Modifier = Modifier
) {
val txIdString = transaction.overview.txIdString()
Column(modifier = modifier) {
if (transaction.expandableState == TrxItemState.EXPANDED_ID ||
transaction.expandableState == TrxItemState.EXPANDED_ALL
) {
Text(
text = stringResource(id = R.string.account_history_item_transaction_id),
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textDescription,
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXtiny))
Text(
text = txIdString,
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textCommon,
modifier =
Modifier
.fillMaxWidth(EXPANDED_TRANSACTION_ID_WIDTH_RATIO)
.testTag(HistoryTag.TRANSACTION_ID)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
TextWithIcon(
text = stringResource(id = R.string.account_history_item_tap_to_copy),
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textDescription,
imageVector = ImageVector.vectorResource(R.drawable.ic_trx_copy),
modifier =
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable { onAction(TrxItemAction.TransactionIdClick(txIdString)) }
.padding(all = ZcashTheme.dimens.spacingTiny)
)
} else {
Row(
modifier =
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable {
onAction(
TrxItemAction.ExpandableStateChange(
transaction.overview.rawId,
if (transaction.expandableState == TrxItemState.EXPANDED_ADDRESS) {
TrxItemState.EXPANDED_ALL
} else {
TrxItemState.EXPANDED_ID
}
)
)
}
.padding(all = ZcashTheme.dimens.spacingTiny)
) {
Text(
text = stringResource(id = R.string.account_history_item_transaction_id),
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textDescription,
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
Text(
text = txIdString,
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textDescription,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.fillMaxWidth(COLLAPSED_TRANSACTION_ID_WIDTH_RATIO)
.testTag(HistoryTag.TRANSACTION_ID)
)
}
}
}
}
internal sealed class TransactionItemAction {
data class IdClick(val id: String) : TransactionItemAction()
@Composable
private fun HistoryItemMessagePart(
messages: ImmutableList<String>,
onAction: (TrxItemAction) -> Unit,
modifier: Modifier = Modifier
) {
val composedMessage = messages.joinToString(separator = "\n\n")
Column(modifier = modifier.then(Modifier.fillMaxWidth())) {
Text(
text = stringResource(id = R.string.account_history_item_message),
style = ZcashTheme.extendedTypography.transactionItemStyles.contentMedium,
color = ZcashTheme.colors.textMedium
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Box(
modifier =
Modifier
.fillMaxWidth()
.border(width = 1.dp, color = ZcashTheme.colors.textFieldFrame)
) {
Text(
text = composedMessage,
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textCommon,
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingMid)
)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
TextWithIcon(
text = stringResource(id = R.string.account_history_item_tap_to_copy),
style = ZcashTheme.extendedTypography.transactionItemStyles.content,
color = ZcashTheme.colors.textDescription,
imageVector = ImageVector.vectorResource(R.drawable.ic_trx_copy),
modifier =
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable { onAction(TrxItemAction.MessageClick(composedMessage)) }
.padding(all = ZcashTheme.dimens.spacingTiny)
)
}
}
internal sealed class TrxItemAction {
data class TransactionIdClick(val id: String) : TrxItemAction()
data class ExpandableStateChange(
val txId: FirstClassByteArray,
val newState: HistoryItemExpandableState
) : TransactionItemAction()
val newState: TrxItemState
) : TrxItemAction()
data class MemoClick(val overview: TransactionOverview) : TransactionItemAction()
data class AddressClick(val address: TransactionRecipient.Address) : TrxItemAction()
data class MessageClick(val memo: String) : TrxItemAction()
}
internal enum class TransactionExtendedState {

View File

@ -3,11 +3,14 @@ package co.electriccoin.zcash.ui.screen.account.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.TransactionOverview
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.screen.account.model.HistoryItemExpandableState
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.account.model.TrxItemState
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -18,6 +21,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toList
class TransactionHistoryViewModel(application: Application) : AndroidViewModel(application) {
private val transactions: MutableStateFlow<ImmutableList<TransactionUi>> = MutableStateFlow(persistentListOf())
@ -41,29 +45,59 @@ class TransactionHistoryViewModel(application: Application) : AndroidViewModel(a
TransactionHistorySyncState.Loading -> persistentListOf()
is TransactionHistorySyncState.Prepared -> {
dataState.transactions.map { data ->
val existingTransaction =
transactions.value.find {
data.overview.rawId == it.overview.rawId
}
TransactionUi.new(
data = data,
expandableState =
transactions.value.find {
data.overview.rawId == it.overview.rawId
}?.expandableState ?: HistoryItemExpandableState.COLLAPSED
expandableState = existingTransaction?.expandableState ?: TrxItemState.COLLAPSED,
messages = existingTransaction?.messages,
)
}.toPersistentList()
}
}
}
fun updateTransactionItemState(
txId: FirstClassByteArray,
newState: HistoryItemExpandableState
) {
private fun updateTransaction(newTransaction: TransactionUi) {
transactions.value =
transactions.value.map { item ->
if (item.overview.rawId == txId) {
item.copy(expandableState = newState)
if (item.overview.rawId == newTransaction.overview.rawId) {
newTransaction
} else {
item
}
}.toPersistentList()
}
suspend fun updateTransactionItemState(
synchronizer: Synchronizer,
txId: FirstClassByteArray,
newState: TrxItemState
) {
val updated =
transactions.value
.find { it.overview.rawId == txId }
?.copy(expandableState = newState)
if (updated != null) {
var updatedWithMessages = updated
// Expanding the item on the first time -> load messages
if (newState == TrxItemState.EXPANDED && updated.messages == null) {
val messages = loadMessageForTransaction(synchronizer, updated.overview)
updatedWithMessages = updated.copy(messages = messages)
}
updateTransaction(updatedWithMessages)
} else {
Twig.warn { "Transaction not found" }
}
}
private suspend fun loadMessageForTransaction(
synchronizer: Synchronizer,
overview: TransactionOverview
): List<String> =
synchronizer.getMemos(overview).toList().also {
Twig.info { "Transaction messages count: ${it.size}" }
}
}

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M1,1h10v10h-10z"
android:strokeWidth="0.25"
android:fillColor="#00000000"
android:strokeColor="#777777"/>
<path
android:strokeWidth="1"
android:pathData="M7.667,6.833L6,5.167L4.333,6.833"
android:fillColor="#00000000"
android:strokeColor="#F4B728"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="11dp"
android:height="10dp"
android:viewportWidth="11"
android:viewportHeight="10">
<group>
<clip-path
android:pathData="M0,0h10.25v9.75h-10.25z"/>
<path
android:pathData="M0,9.75V1.72H8.46V9.75H0ZM7.58,8.9V2.57H0.88V8.91H7.58V8.9Z"
android:fillColor="#000000"/>
<path
android:pathData="M9.81,7.2C9.57,7.2 9.37,7.01 9.37,6.78V0.85H3.1C2.86,0.85 2.66,0.66 2.66,0.43C2.66,0.2 2.86,0.01 3.1,0.01H10.25V6.79C10.25,7.02 10.05,7.21 9.81,7.21V7.2Z"
android:fillColor="#000000"/>
</group>
</vector>

View File

@ -11,8 +11,12 @@
<string name="account_history_item_shielded">Shielded transaction</string>
<string name="account_history_item_sent_prefix">-</string>
<string name="account_history_item_received_prefix">+</string>
<string name="account_history_item_tap_to_copy">Tap to copy</string>
<string name="account_history_item_message">Message</string>
<string name="account_history_item_collapse_transaction">Collapse transaction</string>
<string name="account_history_item_transaction_id">Transaction ID</string>
<string name="account_history_item_clipboard_tag">Transaction memo</string>
<string name="account_history_memo_clipboard_tag">Transaction message</string>
<string name="account_history_id_clipboard_tag">Transaction ID</string>
<string name="account_history_address_clipboard_tag">Transaction address</string>
</resources>