[#1307] Keep transaction history UI state

- Closes #1307
- Changelog update
- #1162 is the direct follow-up of this PR
This commit is contained in:
Honza Rychnovský 2024-03-28 12:57:04 +01:00 committed by GitHub
parent 809820c1f2
commit c99c2907b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 423 additions and 212 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ syntax: glob
.idea/modules.xml .idea/modules.xml
.idea/tasks.xml .idea/tasks.xml
.idea/workspace.xml .idea/workspace.xml
.idea/deploymentTargetSelector.xml
.settings .settings
*.iml *.iml
bin/ bin/

View File

@ -21,7 +21,8 @@ directly impact users rather than highlighting other key architectural updates.*
submission submission
### Changed ### Changed
- The Transaction History UI has been incorporated into the Account screen - The Transaction History UI has been incorporated into the Account screen and partly reworked according to the
design guidelines
- Reworked Send screens flow and their look (e.g., Send Failure screen is now a modal dialog instead of a separate - Reworked Send screens flow and their look (e.g., Send Failure screen is now a modal dialog instead of a separate
screen) screen)
- The sending and shielding funds logic has been connected to the new Proposal API from the Zcash SDK - The sending and shielding funds logic has been connected to the new Proposal API from the Zcash SDK

View File

@ -23,7 +23,7 @@ private fun CircularScreenProgressIndicatorComposablePreview() {
GradientSurface { GradientSurface {
Column { Column {
CircularScreenProgressIndicator() CircularScreenProgressIndicator()
CircularSmallProgressIndicator() CircularMidProgressIndicator()
} }
} }
} }
@ -47,6 +47,18 @@ fun CircularScreenProgressIndicator(modifier: Modifier = Modifier) {
} }
} }
@Composable
fun CircularMidProgressIndicator(modifier: Modifier = Modifier) {
CircularProgressIndicator(
color = ZcashTheme.colors.circularProgressBarScreen,
strokeWidth = 3.dp,
modifier =
Modifier
.size(ZcashTheme.dimens.circularMidProgressWidth)
.then(modifier)
)
}
@Composable @Composable
fun CircularSmallProgressIndicator(modifier: Modifier = Modifier) { fun CircularSmallProgressIndicator(modifier: Modifier = Modifier) {
CircularProgressIndicator( CircularProgressIndicator(

View File

@ -33,6 +33,7 @@ data class Dimens(
val chipStroke: Dp, val chipStroke: Dp,
// Progress // Progress
val circularScreenProgressWidth: Dp, val circularScreenProgressWidth: Dp,
val circularMidProgressWidth: Dp,
val circularSmallProgressWidth: Dp, val circularSmallProgressWidth: Dp,
val linearProgressHeight: Dp, val linearProgressHeight: Dp,
// TopAppBar: // TopAppBar:
@ -75,6 +76,7 @@ private val defaultDimens =
chipShadowElevation = 4.dp, chipShadowElevation = 4.dp,
chipStroke = 0.5.dp, chipStroke = 0.5.dp,
circularScreenProgressWidth = 48.dp, circularScreenProgressWidth = 48.dp,
circularMidProgressWidth = 22.dp,
circularSmallProgressWidth = 14.dp, circularSmallProgressWidth = 14.dp,
linearProgressHeight = 14.dp, linearProgressHeight = 14.dp,
topAppBarZcashLogoHeight = 24.dp, topAppBarZcashLogoHeight = 24.dp,

View File

@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletSnapshot import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistorySyncStateFixture import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistoryUiStateFixture
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState 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.Account
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -16,7 +16,7 @@ class AccountTestSetup(
// TODO [#1282]: Update AccountView Tests #1282 // TODO [#1282]: Update AccountView Tests #1282
// TODO [#1282]: https://github.com/Electric-Coin-Company/zashi-android/issues/1282 // TODO [#1282]: https://github.com/Electric-Coin-Company/zashi-android/issues/1282
val initialHistorySyncState: TransactionHistorySyncState = TransactionHistorySyncStateFixture.new() val initialTransactionState: TransactionUiState = TransactionHistoryUiStateFixture.new()
private val onSettingsCount = AtomicInteger(0) private val onSettingsCount = AtomicInteger(0)
private val onReceiveCount = AtomicInteger(0) private val onReceiveCount = AtomicInteger(0)
@ -64,13 +64,10 @@ class AccountTestSetup(
onSettingsCount.incrementAndGet() onSettingsCount.incrementAndGet()
}, },
goBalances = {}, goBalances = {},
transactionState = initialHistorySyncState, transactionsUiState = initialTransactionState,
onItemClick = { onTransactionItemAction = {
onItemClickCount.incrementAndGet() onItemClickCount.incrementAndGet()
}, },
onTransactionIdClick = {
onItemIdClickCount.incrementAndGet()
}
) )
} }

View File

@ -2,13 +2,13 @@ package co.electriccoin.zcash.ui.screen.account.history
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.account.view.HistoryContainer import co.electriccoin.zcash.ui.screen.account.view.HistoryContainer
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
class HistoryTestSetup( class HistoryTestSetup(
private val composeTestRule: ComposeContentTestRule, private val composeTestRule: ComposeContentTestRule,
initialHistorySyncState: TransactionHistorySyncState initialHistoryUiState: TransactionUiState
) { ) {
private val onItemClickCount = AtomicInteger(0) private val onItemClickCount = AtomicInteger(0)
private val onItemIdClickCount = AtomicInteger(0) private val onItemIdClickCount = AtomicInteger(0)
@ -27,11 +27,8 @@ class HistoryTestSetup(
composeTestRule.setContent { composeTestRule.setContent {
ZcashTheme { ZcashTheme {
HistoryContainer( HistoryContainer(
transactionState = initialHistorySyncState, transactionState = initialHistoryUiState,
onItemClick = { onTransactionItemAction = {
onItemClickCount.incrementAndGet()
},
onTransactionIdClick = {
onItemIdClickCount.incrementAndGet() onItemIdClickCount.incrementAndGet()
} }
) )

View File

@ -3,8 +3,8 @@ package co.electriccoin.zcash.ui.screen.account.history.fixture
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.TransactionRecipient import cash.z.ecc.android.sdk.model.TransactionRecipient
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf

View File

@ -0,0 +1,43 @@
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.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 STATE = TransactionUiState.Prepared(TRANSACTIONS)
fun new(
transactions: ImmutableList<TransactionUi> = TRANSACTIONS,
state: TransactionUiState = STATE
) = when (state) {
is TransactionUiState.Loading -> state
is TransactionUiState.Syncing -> state
is TransactionUiState.Prepared -> {
state.copy(transactions)
}
}
}

View File

@ -11,7 +11,8 @@ import androidx.test.filters.MediumTest
import co.electriccoin.zcash.ui.screen.account.HistoryTag import co.electriccoin.zcash.ui.screen.account.HistoryTag
import co.electriccoin.zcash.ui.screen.account.history.HistoryTestSetup import co.electriccoin.zcash.ui.screen.account.history.HistoryTestSetup
import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistorySyncStateFixture import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistorySyncStateFixture
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistoryUiStateFixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
@ -26,7 +27,7 @@ class HistoryViewTest {
@Test @Test
@MediumTest @MediumTest
fun check_loading_state() { fun check_loading_state() {
newTestSetup(TransactionHistorySyncState.Loading) newTestSetup(TransactionUiState.Loading)
composeTestRule.onNodeWithTag(HistoryTag.PROGRESS).also { composeTestRule.onNodeWithTag(HistoryTag.PROGRESS).also {
it.assertExists() it.assertExists()
@ -37,9 +38,9 @@ class HistoryViewTest {
@MediumTest @MediumTest
fun check_syncing_state() { fun check_syncing_state() {
newTestSetup( newTestSetup(
TransactionHistorySyncStateFixture.new( TransactionHistoryUiStateFixture.new(
state = TransactionHistorySyncStateFixture.STATE, state = TransactionUiState.Prepared(persistentListOf()),
transactions = TransactionHistorySyncStateFixture.TRANSACTIONS transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
) )
) )
@ -60,9 +61,9 @@ class HistoryViewTest {
@MediumTest @MediumTest
fun check_done_state_no_transactions() { fun check_done_state_no_transactions() {
newTestSetup( newTestSetup(
TransactionHistorySyncStateFixture.new( TransactionHistoryUiStateFixture.new(
state = TransactionHistorySyncState.Done(persistentListOf()), state = TransactionUiState.Prepared(persistentListOf()),
transactions = persistentListOf() transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
) )
) )
// composeTestRule.onNodeWithText(getStringResource(R.string.history_syncing)).also { // composeTestRule.onNodeWithText(getStringResource(R.string.history_syncing)).also {
@ -83,9 +84,9 @@ class HistoryViewTest {
@MediumTest @MediumTest
fun check_done_state_with_transactions() { fun check_done_state_with_transactions() {
newTestSetup( newTestSetup(
TransactionHistorySyncStateFixture.new( TransactionHistoryUiStateFixture.new(
state = TransactionHistorySyncState.Done(persistentListOf()), state = TransactionUiState.Prepared(persistentListOf()),
transactions = TransactionHistorySyncStateFixture.TRANSACTIONS transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
) )
) )
// composeTestRule.onNodeWithText(getStringResource(R.string.history_syncing)).also { // composeTestRule.onNodeWithText(getStringResource(R.string.history_syncing)).also {
@ -106,7 +107,7 @@ class HistoryViewTest {
@Test @Test
@MediumTest @MediumTest
fun item_click_test() { fun item_click_test() {
val testSetup = newTestSetup(TransactionHistorySyncStateFixture.STATE) val testSetup = newTestSetup(TransactionHistoryUiStateFixture.STATE)
assertEquals(0, testSetup.getOnItemClickCount()) assertEquals(0, testSetup.getOnItemClickCount())
@ -124,7 +125,7 @@ class HistoryViewTest {
@Test @Test
@MediumTest @MediumTest
fun transaction_id_click_test() { fun transaction_id_click_test() {
val testSetup = newTestSetup(TransactionHistorySyncStateFixture.STATE) val testSetup = newTestSetup(TransactionHistoryUiStateFixture.STATE)
assertEquals(0, testSetup.getOnItemIdClickCount()) assertEquals(0, testSetup.getOnItemIdClickCount())
@ -140,11 +141,11 @@ class HistoryViewTest {
} }
private fun newTestSetup( private fun newTestSetup(
transactionHistorySyncState: TransactionHistorySyncState = TransactionHistorySyncStateFixture.new() transactionHistorySyncState: TransactionUiState = TransactionHistoryUiStateFixture.new()
): HistoryTestSetup { ): HistoryTestSetup {
return HistoryTestSetup( return HistoryTestSetup(
composeTestRule = composeTestRule, composeTestRule = composeTestRule,
initialHistorySyncState = transactionHistorySyncState initialHistoryUiState = transactionHistorySyncState
) )
} }
} }

View File

@ -34,9 +34,9 @@ import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.state.getSortHeight
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi

View File

@ -13,6 +13,8 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.account.view.Account 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.viewmodel.TransactionHistoryViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -26,17 +28,24 @@ internal fun WrapAccount(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val transactionHistoryViewModel by activity.viewModels<TransactionHistoryViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>() val settingsViewModel by activity.viewModels<SettingsViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val transactionHistoryState = walletViewModel.transactionHistoryState.collectAsStateWithLifecycle().value val transactionHistoryState = walletViewModel.transactionHistoryState.collectAsStateWithLifecycle().value
Twig.info { "Current transaction history state: $transactionHistoryState" } val transactionsUiState = transactionHistoryViewModel.transactionUiState.collectAsStateWithLifecycle().value
Twig.info { "Current transaction history state: $transactionsUiState" }
transactionHistoryViewModel.processTransactionState(transactionHistoryState)
if (null == walletSnapshot) { if (null == walletSnapshot) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer // TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
@ -47,30 +56,37 @@ internal fun WrapAccount(
Account( Account(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing, isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
transactionState = transactionHistoryState, transactionsUiState = transactionsUiState,
onItemClick = { tx -> onTransactionItemAction = { action ->
Twig.debug { "Transaction item clicked - querying memos..." } when (action) {
val memos = synchronizer?.getMemos(tx.overview) is TransactionItemAction.IdClick -> {
scope.launch { Twig.info { "Transaction ID clicked: ${action.id}" }
memos?.toList()?.let {
val merged = it.joinToString().ifEmpty { "-" }
Twig.info { "Transaction memos: count: ${it.size}, contains: $merged" }
ClipboardManagerUtil.copyToClipboard( ClipboardManagerUtil.copyToClipboard(
activity.applicationContext, activity.applicationContext,
activity.getString(R.string.account_history_item_clipboard_tag), activity.getString(R.string.account_history_id_clipboard_tag),
merged action.id
) )
} }
is TransactionItemAction.MemoClick -> {
Twig.info { "Transaction item clicked - querying memos..." }
val memos = synchronizer?.getMemos(action.overview)
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
)
}
}
}
is TransactionItemAction.ExpandableStateChange -> {
transactionHistoryViewModel.updateTransactionItemState(action.txId, action.newState)
}
} }
}, },
onTransactionIdClick = { txId ->
Twig.debug { "Transaction ID clicked: $txId" }
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.account_history_id_clipboard_tag),
txId
)
},
goBalances = goBalances, goBalances = goBalances,
goSettings = goSettings, goSettings = goSettings,
) )

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.screen.account.state package co.electriccoin.zcash.ui.screen.account.ext
import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.TransactionOverview

View File

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

View File

@ -0,0 +1,22 @@
package co.electriccoin.zcash.ui.screen.account.model
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
data class TransactionUi(
val overview: TransactionOverview,
val recipient: TransactionRecipient?,
val expandableState: HistoryItemExpandableState
) {
companion object {
fun new(
data: TransactionOverviewExt,
expandableState: HistoryItemExpandableState
) = TransactionUi(
overview = data.overview,
recipient = data.recipient,
expandableState = expandableState
)
}
}

View File

@ -0,0 +1,11 @@
package co.electriccoin.zcash.ui.screen.account.model
import kotlinx.collections.immutable.ImmutableList
sealed interface TransactionUiState {
data object Loading : TransactionUiState
data object Syncing : TransactionUiState
data class Prepared(val transactions: ImmutableList<TransactionUi>) : TransactionUiState
}

View File

@ -1,21 +1,14 @@
package co.electriccoin.zcash.ui.screen.account.state package co.electriccoin.zcash.ui.screen.account.state
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
sealed class TransactionHistorySyncState { sealed interface TransactionHistorySyncState {
object Loading : TransactionHistorySyncState() { data object Loading : TransactionHistorySyncState
override fun toString() = "Loading" // NON-NLS
}
data class Syncing(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState() { sealed class Prepared(open val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState
fun hasNoTransactions(): Boolean {
return transactions.isEmpty()
}
}
data class Done(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState() { data class Syncing(override val transactions: ImmutableList<TransactionOverviewExt>) : Prepared(transactions)
fun hasNoTransactions(): Boolean {
return transactions.isEmpty() data class Done(override val transactions: ImmutableList<TransactionOverviewExt>) : Prepared(transactions)
}
}
} }

View File

@ -28,8 +28,9 @@ import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.model.HistoryItemExpandableState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Preview("Account No History") @Preview("Account No History")
@ -42,9 +43,8 @@ private fun HistoryLoadingComposablePreview() {
isKeepScreenOnWhileSyncing = false, isKeepScreenOnWhileSyncing = false,
goBalances = {}, goBalances = {},
goSettings = {}, goSettings = {},
transactionState = TransactionHistorySyncState.Loading, transactionsUiState = TransactionUiState.Loading,
onItemClick = {}, onTransactionItemAction = {}
onTransactionIdClick = {}
) )
} }
} }
@ -55,31 +55,34 @@ private fun HistoryLoadingComposablePreview() {
private fun HistoryListComposablePreview() { private fun HistoryListComposablePreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
@Suppress("MagicNumber")
Account( Account(
walletSnapshot = WalletSnapshotFixture.new(), walletSnapshot = WalletSnapshotFixture.new(),
isKeepScreenOnWhileSyncing = false, isKeepScreenOnWhileSyncing = false,
goBalances = {}, goBalances = {},
goSettings = {}, goSettings = {},
transactionState = transactionsUiState =
TransactionHistorySyncState.Syncing( TransactionUiState.Prepared(
@Suppress("MagicNumber") transactions =
persistentListOf( persistentListOf(
TransactionOverviewExt( TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)), TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
null null,
), HistoryItemExpandableState.EXPANDED
TransactionOverviewExt( ),
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)), TransactionUi(
null TransactionOverviewFixture.new(netValue = Zatoshi(200000000)),
), null,
TransactionOverviewExt( HistoryItemExpandableState.COLLAPSED
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)), ),
null TransactionUi(
), TransactionOverviewFixture.new(netValue = Zatoshi(300000000)),
) null,
HistoryItemExpandableState.COLLAPSED
),
)
), ),
onItemClick = {}, onTransactionItemAction = {},
onTransactionIdClick = {}
) )
} }
} }
@ -87,13 +90,12 @@ private fun HistoryListComposablePreview() {
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun Account( internal fun Account(
goBalances: () -> Unit, goBalances: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
isKeepScreenOnWhileSyncing: Boolean?, isKeepScreenOnWhileSyncing: Boolean?,
onItemClick: (TransactionOverviewExt) -> Unit, onTransactionItemAction: (TransactionItemAction) -> Unit,
onTransactionIdClick: (String) -> Unit, transactionsUiState: TransactionUiState,
transactionState: TransactionHistorySyncState,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
@ -103,9 +105,8 @@ fun Account(
walletSnapshot = walletSnapshot, walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing, isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
goBalances = goBalances, goBalances = goBalances,
transactionState = transactionState, transactionState = transactionsUiState,
onItemClick = onItemClick, onTransactionItemAction = onTransactionItemAction,
onTransactionIdClick = onTransactionIdClick,
modifier = modifier =
Modifier.padding( Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault, top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
@ -140,9 +141,8 @@ private fun AccountMainContent(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isKeepScreenOnWhileSyncing: Boolean?, isKeepScreenOnWhileSyncing: Boolean?,
goBalances: () -> Unit, goBalances: () -> Unit,
onItemClick: (TransactionOverviewExt) -> Unit, onTransactionItemAction: (TransactionItemAction) -> Unit,
onTransactionIdClick: (String) -> Unit, transactionState: TransactionUiState,
transactionState: TransactionHistorySyncState,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Column( Column(
@ -163,8 +163,7 @@ private fun AccountMainContent(
HistoryContainer( HistoryContainer(
transactionState = transactionState, transactionState = transactionState,
onItemClick = onItemClick, onTransactionItemAction = onTransactionItemAction,
onTransactionIdClick = onTransactionIdClick,
) )
if (isKeepScreenOnWhileSyncing == true && walletSnapshot.status == Synchronizer.Status.SYNCING) { if (isKeepScreenOnWhileSyncing == true && walletSnapshot.status == Synchronizer.Status.SYNCING) {

View File

@ -14,19 +14,13 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.DividerDefaults import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -38,20 +32,22 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture 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.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.TransactionState import cash.z.ecc.android.sdk.model.TransactionState
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.toZecString import cash.z.ecc.android.sdk.model.toZecString
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularMidProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.StyledBalance import co.electriccoin.zcash.ui.design.component.StyledBalance
import co.electriccoin.zcash.ui.design.component.Tiny import co.electriccoin.zcash.ui.design.component.Tiny
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.HistoryTag import co.electriccoin.zcash.ui.screen.account.HistoryTag
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.model.HistoryItemExpandableState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt 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.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import java.text.DateFormat import java.text.DateFormat
@ -64,9 +60,8 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
HistoryContainer( HistoryContainer(
transactionState = TransactionHistorySyncState.Loading, transactionState = TransactionUiState.Loading,
onItemClick = {}, onTransactionItemAction = {}
onTransactionIdClick = {}
) )
} }
} }
@ -77,27 +72,30 @@ private fun ComposablePreview() {
private fun ComposableHistoryListPreview() { private fun ComposableHistoryListPreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
GradientSurface { GradientSurface {
@Suppress("MagicNumber")
HistoryContainer( HistoryContainer(
transactionState = transactionState =
TransactionHistorySyncState.Syncing( TransactionUiState.Prepared(
@Suppress("MagicNumber") transactions =
persistentListOf( persistentListOf(
TransactionOverviewExt( TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)), TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
null null,
), HistoryItemExpandableState.EXPANDED
TransactionOverviewExt( ),
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)), TransactionUi(
null TransactionOverviewFixture.new(netValue = Zatoshi(200000000)),
), null,
TransactionOverviewExt( HistoryItemExpandableState.COLLAPSED
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)), ),
null TransactionUi(
), TransactionOverviewFixture.new(netValue = Zatoshi(300000000)),
) null,
HistoryItemExpandableState.COLLAPSED
),
)
), ),
onItemClick = {}, onTransactionItemAction = {}
onTransactionIdClick = {}
) )
} }
} }
@ -114,11 +112,9 @@ val dateFormat: DateFormat by lazy {
} }
@Composable @Composable
@Suppress("LongMethod") internal fun HistoryContainer(
fun HistoryContainer( transactionState: TransactionUiState,
transactionState: TransactionHistorySyncState, onTransactionItemAction: (TransactionItemAction) -> Unit,
onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Box( Box(
@ -131,27 +127,37 @@ fun HistoryContainer(
) )
) { ) {
when (transactionState) { when (transactionState) {
is TransactionHistorySyncState.Loading -> { TransactionUiState.Loading, TransactionUiState.Syncing -> {
CircularScreenProgressIndicator( Column(
modifier = horizontalAlignment = Alignment.CenterHorizontally,
Modifier modifier = Modifier.fillMaxWidth()
.align(alignment = Center) ) {
.testTag(HistoryTag.PROGRESS) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
) CircularMidProgressIndicator(
modifier = Modifier.testTag(HistoryTag.PROGRESS),
)
}
} }
is TransactionHistorySyncState.Syncing -> { is TransactionUiState.Prepared -> {
HistoryList( if (transactionState.transactions.isEmpty()) {
transactions = transactionState.transactions, Column {
onItemClick = onItemClick, Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
onTransactionIdClick = onTransactionIdClick Text(
) modifier = Modifier.fillMaxWidth(),
} textAlign = TextAlign.Center,
is TransactionHistorySyncState.Done -> { text = stringResource(id = R.string.account_history_empty),
HistoryList( style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular,
transactions = transactionState.transactions, color = ZcashTheme.colors.textCommon,
onItemClick = onItemClick, maxLines = 1,
onTransactionIdClick = onTransactionIdClick overflow = TextOverflow.Ellipsis,
) )
}
} else {
HistoryList(
transactions = transactionState.transactions,
onAction = onTransactionItemAction,
)
}
} }
} }
} }
@ -159,60 +165,53 @@ fun HistoryContainer(
@Composable @Composable
private fun HistoryList( private fun HistoryList(
transactions: ImmutableList<TransactionOverviewExt>, transactions: ImmutableList<TransactionUi>,
onItemClick: (TransactionOverviewExt) -> Unit, onAction: (TransactionItemAction) -> Unit
onTransactionIdClick: (String) -> Unit
) { ) {
if (transactions.isEmpty()) { LazyColumn(
Column { modifier = Modifier.testTag(HistoryTag.TRANSACTION_LIST)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) ) {
items(transactions.size) { index ->
Text( HistoryItem(
modifier = Modifier.fillMaxWidth(), transaction = transactions[index],
textAlign = TextAlign.Center, onAction = onAction
text = stringResource(id = R.string.account_history_empty),
style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular,
color = ZcashTheme.colors.textCommon,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
}
} else {
LazyColumn(
modifier = Modifier.testTag(HistoryTag.TRANSACTION_LIST)
) {
itemsIndexed(transactions) { _, item ->
HistoryItem(
transaction = item,
onItemClick = onItemClick,
onIdClick = onTransactionIdClick,
)
Divider( Divider(
color = ZcashTheme.colors.dividerColor, color = ZcashTheme.colors.dividerColor,
thickness = DividerDefaults.Thickness, thickness = DividerDefaults.Thickness,
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault) modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault)
) )
}
} }
} }
} }
private enum class ItemExpandedState { @Composable
COLLAPSED, @Preview("History List Item")
EXPANDED, private fun ComposableHistoryListItemPreview() {
EXPANDED_ADDRESS, ZcashTheme(forceDarkMode = false) {
EXPANDED_ID GradientSurface {
@Suppress("MagicNumber")
HistoryItem(
onAction = {},
transaction =
TransactionUi(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
recipient = null,
expandableState = HistoryItemExpandableState.EXPANDED
)
)
}
}
} }
const val ADDRESS_IN_TITLE_WIDTH_RATIO = 0.5f const val ADDRESS_IN_TITLE_WIDTH_RATIO = 0.5f
@Composable @Composable
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
fun HistoryItem( private fun HistoryItem(
transaction: TransactionOverviewExt, transaction: TransactionUi,
onItemClick: (TransactionOverviewExt) -> Unit, onAction: (TransactionItemAction) -> Unit,
onIdClick: (String) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val typeText: String val typeText: String
@ -259,10 +258,6 @@ fun HistoryItem(
} }
} }
var expandedState: ItemExpandedState by rememberSaveable {
mutableStateOf(ItemExpandedState.COLLAPSED)
}
Row( Row(
modifier = modifier =
modifier modifier
@ -270,10 +265,14 @@ fun HistoryItem(
Modifier Modifier
.background(color = ZcashTheme.colors.historyBackgroundColor) .background(color = ZcashTheme.colors.historyBackgroundColor)
.clickable { .clickable {
if (expandedState == ItemExpandedState.COLLAPSED) { if (transaction.expandableState <= HistoryItemExpandableState.COLLAPSED) {
expandedState = ItemExpandedState.EXPANDED onAction(
TransactionItemAction.ExpandableStateChange(
transaction.overview.rawId,
HistoryItemExpandableState.EXPANDED
)
)
} }
onItemClick(transaction)
} }
.padding(all = ZcashTheme.dimens.spacingLarge) .padding(all = ZcashTheme.dimens.spacingLarge)
.animateContentSize() .animateContentSize()
@ -363,7 +362,7 @@ fun HistoryItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
if (expandedState >= ItemExpandedState.EXPANDED) { if (transaction.expandableState == HistoryItemExpandableState.EXPANDED) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
val txId = transaction.overview.txIdString() val txId = transaction.overview.txIdString()
@ -371,15 +370,54 @@ fun HistoryItem(
text = txId, text = txId,
modifier = modifier =
Modifier Modifier
.clickable { onIdClick(txId) } .clickable { onAction(TransactionItemAction.IdClick(txId)) }
.testTag(HistoryTag.TRANSACTION_ID) .testTag(HistoryTag.TRANSACTION_ID)
) )
Spacer(modifier = (Modifier.height(ZcashTheme.dimens.spacingDefault)))
// 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)))
Tiny(
text = stringResource(id = R.string.account_history_item_collapse_transaction),
modifier =
Modifier
.clickable {
if (transaction.expandableState >= HistoryItemExpandableState.EXPANDED) {
onAction(
TransactionItemAction.ExpandableStateChange(
transaction.overview.rawId,
HistoryItemExpandableState.COLLAPSED
)
)
}
}
)
} }
} }
} }
} }
enum class TransactionExtendedState { internal sealed class TransactionItemAction {
data class IdClick(val id: String) : TransactionItemAction()
data class ExpandableStateChange(
val txId: FirstClassByteArray,
val newState: HistoryItemExpandableState
) : TransactionItemAction()
data class MemoClick(val overview: TransactionOverview) : TransactionItemAction()
}
internal enum class TransactionExtendedState {
SENT, SENT,
SENDING, SENDING,
SEND_FAILED, SEND_FAILED,

View File

@ -0,0 +1,69 @@
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.model.FirstClassByteArray
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.state.TransactionHistorySyncState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class TransactionHistoryViewModel(application: Application) : AndroidViewModel(application) {
private val transactions: MutableStateFlow<ImmutableList<TransactionUi>> = MutableStateFlow(persistentListOf())
val transactionUiState: StateFlow<TransactionUiState> =
transactions.map {
if (it.isEmpty()) {
TransactionUiState.Syncing
} else {
TransactionUiState.Prepared(it)
}
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
TransactionUiState.Loading
)
fun processTransactionState(dataState: TransactionHistorySyncState) {
transactions.value =
when (dataState) {
TransactionHistorySyncState.Loading -> persistentListOf()
is TransactionHistorySyncState.Prepared -> {
dataState.transactions.map { data ->
TransactionUi.new(
data = data,
expandableState =
transactions.value.find {
data.overview.rawId == it.overview.rawId
}?.expandableState ?: HistoryItemExpandableState.COLLAPSED
)
}.toPersistentList()
}
}
}
fun updateTransactionItemState(
txId: FirstClassByteArray,
newState: HistoryItemExpandableState
) {
transactions.value =
transactions.value.map { item ->
if (item.overview.rawId == txId) {
item.copy(expandableState = newState)
} else {
item
}
}.toPersistentList()
}
}

View File

@ -11,6 +11,7 @@
<string name="account_history_item_shielded">Shielded transaction</string> <string name="account_history_item_shielded">Shielded transaction</string>
<string name="account_history_item_sent_prefix">-</string> <string name="account_history_item_sent_prefix">-</string>
<string name="account_history_item_received_prefix">+</string> <string name="account_history_item_received_prefix">+</string>
<string name="account_history_item_collapse_transaction">Collapse transaction</string>
<string name="account_history_item_clipboard_tag">Transaction memo</string> <string name="account_history_item_clipboard_tag">Transaction memo</string>
<string name="account_history_id_clipboard_tag">Transaction ID</string> <string name="account_history_id_clipboard_tag">Transaction ID</string>