[#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/tasks.xml
.idea/workspace.xml
.idea/deploymentTargetSelector.xml
.settings
*.iml
bin/

View File

@ -21,7 +21,8 @@ directly impact users rather than highlighting other key architectural updates.*
submission
### 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
screen)
- 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 {
Column {
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
fun CircularSmallProgressIndicator(modifier: Modifier = Modifier) {
CircularProgressIndicator(

View File

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

View File

@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
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.state.TransactionHistorySyncState
import co.electriccoin.zcash.ui.screen.account.history.fixture.TransactionHistoryUiStateFixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.account.view.Account
import java.util.concurrent.atomic.AtomicInteger
@ -16,7 +16,7 @@ class AccountTestSetup(
// TODO [#1282]: Update AccountView Tests #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 onReceiveCount = AtomicInteger(0)
@ -64,13 +64,10 @@ class AccountTestSetup(
onSettingsCount.incrementAndGet()
},
goBalances = {},
transactionState = initialHistorySyncState,
onItemClick = {
transactionsUiState = initialTransactionState,
onTransactionItemAction = {
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 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 java.util.concurrent.atomic.AtomicInteger
class HistoryTestSetup(
private val composeTestRule: ComposeContentTestRule,
initialHistorySyncState: TransactionHistorySyncState
initialHistoryUiState: TransactionUiState
) {
private val onItemClickCount = AtomicInteger(0)
private val onItemIdClickCount = AtomicInteger(0)
@ -27,11 +27,8 @@ class HistoryTestSetup(
composeTestRule.setContent {
ZcashTheme {
HistoryContainer(
transactionState = initialHistorySyncState,
onItemClick = {
onItemClickCount.incrementAndGet()
},
onTransactionIdClick = {
transactionState = initialHistoryUiState,
onTransactionItemAction = {
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.model.Account
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.TransactionOverviewExt
import kotlinx.collections.immutable.ImmutableList
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.history.HistoryTestSetup
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 org.junit.Assert.assertEquals
import org.junit.Rule
@ -26,7 +27,7 @@ class HistoryViewTest {
@Test
@MediumTest
fun check_loading_state() {
newTestSetup(TransactionHistorySyncState.Loading)
newTestSetup(TransactionUiState.Loading)
composeTestRule.onNodeWithTag(HistoryTag.PROGRESS).also {
it.assertExists()
@ -37,9 +38,9 @@ class HistoryViewTest {
@MediumTest
fun check_syncing_state() {
newTestSetup(
TransactionHistorySyncStateFixture.new(
state = TransactionHistorySyncStateFixture.STATE,
transactions = TransactionHistorySyncStateFixture.TRANSACTIONS
TransactionHistoryUiStateFixture.new(
state = TransactionUiState.Prepared(persistentListOf()),
transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
)
)
@ -60,9 +61,9 @@ class HistoryViewTest {
@MediumTest
fun check_done_state_no_transactions() {
newTestSetup(
TransactionHistorySyncStateFixture.new(
state = TransactionHistorySyncState.Done(persistentListOf()),
transactions = persistentListOf()
TransactionHistoryUiStateFixture.new(
state = TransactionUiState.Prepared(persistentListOf()),
transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
)
)
// composeTestRule.onNodeWithText(getStringResource(R.string.history_syncing)).also {
@ -83,9 +84,9 @@ class HistoryViewTest {
@MediumTest
fun check_done_state_with_transactions() {
newTestSetup(
TransactionHistorySyncStateFixture.new(
state = TransactionHistorySyncState.Done(persistentListOf()),
transactions = TransactionHistorySyncStateFixture.TRANSACTIONS
TransactionHistoryUiStateFixture.new(
state = TransactionUiState.Prepared(persistentListOf()),
transactions = TransactionHistoryUiStateFixture.TRANSACTIONS
)
)
// composeTestRule.onNodeWithText(getStringResource(R.string.history_syncing)).also {
@ -106,7 +107,7 @@ class HistoryViewTest {
@Test
@MediumTest
fun item_click_test() {
val testSetup = newTestSetup(TransactionHistorySyncStateFixture.STATE)
val testSetup = newTestSetup(TransactionHistoryUiStateFixture.STATE)
assertEquals(0, testSetup.getOnItemClickCount())
@ -124,7 +125,7 @@ class HistoryViewTest {
@Test
@MediumTest
fun transaction_id_click_test() {
val testSetup = newTestSetup(TransactionHistorySyncStateFixture.STATE)
val testSetup = newTestSetup(TransactionHistoryUiStateFixture.STATE)
assertEquals(0, testSetup.getOnItemIdClickCount())
@ -140,11 +141,11 @@ class HistoryViewTest {
}
private fun newTestSetup(
transactionHistorySyncState: TransactionHistorySyncState = TransactionHistorySyncStateFixture.new()
transactionHistorySyncState: TransactionUiState = TransactionHistoryUiStateFixture.new()
): HistoryTestSetup {
return HistoryTestSetup(
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.StandardPreferenceKeys
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.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.state.getSortHeight
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
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.design.component.CircularScreenProgressIndicator
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 kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
@ -26,17 +28,24 @@ internal fun WrapAccount(
val scope = rememberCoroutineScope()
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 walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val synchronizer = walletViewModel.synchronizer.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) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
@ -47,30 +56,37 @@ internal fun WrapAccount(
Account(
walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
transactionState = transactionHistoryState,
onItemClick = { tx ->
Twig.debug { "Transaction item clicked - querying memos..." }
val memos = synchronizer?.getMemos(tx.overview)
scope.launch {
memos?.toList()?.let {
val merged = it.joinToString().ifEmpty { "-" }
Twig.info { "Transaction memos: count: ${it.size}, contains: $merged" }
transactionsUiState = transactionsUiState,
onTransactionItemAction = { action ->
when (action) {
is TransactionItemAction.IdClick -> {
Twig.info { "Transaction ID clicked: ${action.id}" }
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.account_history_item_clipboard_tag),
merged
activity.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)
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,
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.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
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import kotlinx.collections.immutable.ImmutableList
sealed class TransactionHistorySyncState {
object Loading : TransactionHistorySyncState() {
override fun toString() = "Loading" // NON-NLS
}
sealed interface TransactionHistorySyncState {
data object Loading : TransactionHistorySyncState
data class Syncing(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState() {
fun hasNoTransactions(): Boolean {
return transactions.isEmpty()
}
}
sealed class Prepared(open val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState
data class Done(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState() {
fun hasNoTransactions(): Boolean {
return transactions.isEmpty()
}
}
data class Syncing(override val transactions: ImmutableList<TransactionOverviewExt>) : Prepared(transactions)
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.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag
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.model.HistoryItemExpandableState
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import kotlinx.collections.immutable.persistentListOf
@Preview("Account No History")
@ -42,9 +43,8 @@ private fun HistoryLoadingComposablePreview() {
isKeepScreenOnWhileSyncing = false,
goBalances = {},
goSettings = {},
transactionState = TransactionHistorySyncState.Loading,
onItemClick = {},
onTransactionIdClick = {}
transactionsUiState = TransactionUiState.Loading,
onTransactionItemAction = {}
)
}
}
@ -55,31 +55,34 @@ private fun HistoryLoadingComposablePreview() {
private fun HistoryListComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
@Suppress("MagicNumber")
Account(
walletSnapshot = WalletSnapshotFixture.new(),
isKeepScreenOnWhileSyncing = false,
goBalances = {},
goSettings = {},
transactionState =
TransactionHistorySyncState.Syncing(
@Suppress("MagicNumber")
persistentListOf(
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
null
),
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)),
null
),
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)),
null
),
)
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
),
)
),
onItemClick = {},
onTransactionIdClick = {}
onTransactionItemAction = {},
)
}
}
@ -87,13 +90,12 @@ private fun HistoryListComposablePreview() {
@Composable
@Suppress("LongParameterList")
fun Account(
internal fun Account(
goBalances: () -> Unit,
goSettings: () -> Unit,
isKeepScreenOnWhileSyncing: Boolean?,
onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit,
transactionState: TransactionHistorySyncState,
onTransactionItemAction: (TransactionItemAction) -> Unit,
transactionsUiState: TransactionUiState,
walletSnapshot: WalletSnapshot,
) {
Scaffold(topBar = {
@ -103,9 +105,8 @@ fun Account(
walletSnapshot = walletSnapshot,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
goBalances = goBalances,
transactionState = transactionState,
onItemClick = onItemClick,
onTransactionIdClick = onTransactionIdClick,
transactionState = transactionsUiState,
onTransactionItemAction = onTransactionItemAction,
modifier =
Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
@ -140,9 +141,8 @@ private fun AccountMainContent(
walletSnapshot: WalletSnapshot,
isKeepScreenOnWhileSyncing: Boolean?,
goBalances: () -> Unit,
onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit,
transactionState: TransactionHistorySyncState,
onTransactionItemAction: (TransactionItemAction) -> Unit,
transactionState: TransactionUiState,
modifier: Modifier = Modifier
) {
Column(
@ -163,8 +163,7 @@ private fun AccountMainContent(
HistoryContainer(
transactionState = transactionState,
onItemClick = onItemClick,
onTransactionIdClick = onTransactionIdClick,
onTransactionItemAction = onTransactionItemAction,
)
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.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Divider
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.Companion.Center
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.tooling.preview.Preview
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
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.CircularScreenProgressIndicator
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.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.HistoryTag
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.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
import java.text.DateFormat
@ -64,9 +60,8 @@ private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
HistoryContainer(
transactionState = TransactionHistorySyncState.Loading,
onItemClick = {},
onTransactionIdClick = {}
transactionState = TransactionUiState.Loading,
onTransactionItemAction = {}
)
}
}
@ -77,27 +72,30 @@ private fun ComposablePreview() {
private fun ComposableHistoryListPreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
@Suppress("MagicNumber")
HistoryContainer(
transactionState =
TransactionHistorySyncState.Syncing(
@Suppress("MagicNumber")
persistentListOf(
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
null
),
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)),
null
),
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)),
null
),
)
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
),
)
),
onItemClick = {},
onTransactionIdClick = {}
onTransactionItemAction = {}
)
}
}
@ -114,11 +112,9 @@ val dateFormat: DateFormat by lazy {
}
@Composable
@Suppress("LongMethod")
fun HistoryContainer(
transactionState: TransactionHistorySyncState,
onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit,
internal fun HistoryContainer(
transactionState: TransactionUiState,
onTransactionItemAction: (TransactionItemAction) -> Unit,
modifier: Modifier = Modifier
) {
Box(
@ -131,27 +127,37 @@ fun HistoryContainer(
)
) {
when (transactionState) {
is TransactionHistorySyncState.Loading -> {
CircularScreenProgressIndicator(
modifier =
Modifier
.align(alignment = Center)
.testTag(HistoryTag.PROGRESS)
)
TransactionUiState.Loading, TransactionUiState.Syncing -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
CircularMidProgressIndicator(
modifier = Modifier.testTag(HistoryTag.PROGRESS),
)
}
}
is TransactionHistorySyncState.Syncing -> {
HistoryList(
transactions = transactionState.transactions,
onItemClick = onItemClick,
onTransactionIdClick = onTransactionIdClick
)
}
is TransactionHistorySyncState.Done -> {
HistoryList(
transactions = transactionState.transactions,
onItemClick = onItemClick,
onTransactionIdClick = onTransactionIdClick
)
is TransactionUiState.Prepared -> {
if (transactionState.transactions.isEmpty()) {
Column {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(id = R.string.account_history_empty),
style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular,
color = ZcashTheme.colors.textCommon,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
} else {
HistoryList(
transactions = transactionState.transactions,
onAction = onTransactionItemAction,
)
}
}
}
}
@ -159,60 +165,53 @@ fun HistoryContainer(
@Composable
private fun HistoryList(
transactions: ImmutableList<TransactionOverviewExt>,
onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit
transactions: ImmutableList<TransactionUi>,
onAction: (TransactionItemAction) -> Unit
) {
if (transactions.isEmpty()) {
Column {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(id = R.string.account_history_empty),
style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular,
color = ZcashTheme.colors.textCommon,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
LazyColumn(
modifier = Modifier.testTag(HistoryTag.TRANSACTION_LIST)
) {
items(transactions.size) { index ->
HistoryItem(
transaction = transactions[index],
onAction = onAction
)
}
} else {
LazyColumn(
modifier = Modifier.testTag(HistoryTag.TRANSACTION_LIST)
) {
itemsIndexed(transactions) { _, item ->
HistoryItem(
transaction = item,
onItemClick = onItemClick,
onIdClick = onTransactionIdClick,
)
Divider(
color = ZcashTheme.colors.dividerColor,
thickness = DividerDefaults.Thickness,
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault)
)
}
Divider(
color = ZcashTheme.colors.dividerColor,
thickness = DividerDefaults.Thickness,
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault)
)
}
}
}
private enum class ItemExpandedState {
COLLAPSED,
EXPANDED,
EXPANDED_ADDRESS,
EXPANDED_ID
@Composable
@Preview("History List Item")
private fun ComposableHistoryListItemPreview() {
ZcashTheme(forceDarkMode = false) {
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
@Composable
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun HistoryItem(
transaction: TransactionOverviewExt,
onItemClick: (TransactionOverviewExt) -> Unit,
onIdClick: (String) -> Unit,
private fun HistoryItem(
transaction: TransactionUi,
onAction: (TransactionItemAction) -> Unit,
modifier: Modifier = Modifier
) {
val typeText: String
@ -259,10 +258,6 @@ fun HistoryItem(
}
}
var expandedState: ItemExpandedState by rememberSaveable {
mutableStateOf(ItemExpandedState.COLLAPSED)
}
Row(
modifier =
modifier
@ -270,10 +265,14 @@ fun HistoryItem(
Modifier
.background(color = ZcashTheme.colors.historyBackgroundColor)
.clickable {
if (expandedState == ItemExpandedState.COLLAPSED) {
expandedState = ItemExpandedState.EXPANDED
if (transaction.expandableState <= HistoryItemExpandableState.COLLAPSED) {
onAction(
TransactionItemAction.ExpandableStateChange(
transaction.overview.rawId,
HistoryItemExpandableState.EXPANDED
)
)
}
onItemClick(transaction)
}
.padding(all = ZcashTheme.dimens.spacingLarge)
.animateContentSize()
@ -363,7 +362,7 @@ fun HistoryItem(
overflow = TextOverflow.Ellipsis,
)
if (expandedState >= ItemExpandedState.EXPANDED) {
if (transaction.expandableState == HistoryItemExpandableState.EXPANDED) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
val txId = transaction.overview.txIdString()
@ -371,15 +370,54 @@ fun HistoryItem(
text = txId,
modifier =
Modifier
.clickable { onIdClick(txId) }
.clickable { onAction(TransactionItemAction.IdClick(txId)) }
.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,
SENDING,
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_sent_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_id_clipboard_tag">Transaction ID</string>