Receive screen account expanded state is now reset on account change

This commit is contained in:
Milan Cerovsky 2024-12-18 08:57:20 +01:00 committed by Milan
parent 9ae240ce29
commit 1580f7045c
6 changed files with 70 additions and 50 deletions

View File

@ -30,6 +30,7 @@ import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveCurrentTransactionsUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveCurrentTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveIsFlexaAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveIsFlexaAvailableUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveOnAccountChangedUseCase
import co.electriccoin.zcash.ui.common.usecase.ObservePersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.ObservePersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase
@ -122,4 +123,5 @@ val useCaseModule =
factoryOf(::ObserveTransactionProposalUseCase) factoryOf(::ObserveTransactionProposalUseCase)
factoryOf(::SharePCZTUseCase) factoryOf(::SharePCZTUseCase)
factoryOf(::CreateKeystoneProposalPCZTEncoderUseCase) factoryOf(::CreateKeystoneProposalPCZTEncoderUseCase)
factoryOf(::ObserveOnAccountChangedUseCase)
} }

View File

@ -25,10 +25,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@ -37,11 +39,14 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retryWhen import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
interface AccountDataSource { interface AccountDataSource {
val onAccountChanged: Flow<Unit>
val allAccounts: StateFlow<List<WalletAccount>?> val allAccounts: StateFlow<List<WalletAccount>?>
val selectedAccount: Flow<WalletAccount?> val selectedAccount: Flow<WalletAccount?>
@ -72,6 +77,8 @@ class AccountDataSourceImpl(
) : AccountDataSource { ) : AccountDataSource {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override val onAccountChanged = MutableSharedFlow<Unit>()
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val internalAccounts: Flow<List<InternalAccountWithBalances>?> = private val internalAccounts: Flow<List<InternalAccountWithBalances>?> =
synchronizerProvider synchronizerProvider
@ -199,12 +206,14 @@ class AccountDataSourceImpl(
.map { account -> .map { account ->
account?.firstOrNull { it.isSelected } account?.firstOrNull { it.isSelected }
} }
.distinctUntilChanged()
override val zashiAccount: Flow<ZashiAccount?> = override val zashiAccount: Flow<ZashiAccount?> =
allAccounts allAccounts
.map { account -> .map { account ->
account?.filterIsInstance<ZashiAccount>()?.firstOrNull() account?.filterIsInstance<ZashiAccount>()?.firstOrNull()
} }
.distinctUntilChanged()
override suspend fun getAllAccounts() = override suspend fun getAllAccounts() =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -221,15 +230,21 @@ class AccountDataSourceImpl(
zashiAccount.filterNotNull().first() zashiAccount.filterNotNull().first()
} }
override suspend fun selectAccount(account: Account) = override suspend fun selectAccount(account: Account) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val current = selectedAccountUUIDProvider.getUUID()
selectedAccountUUIDProvider.setUUID(account.accountUuid) selectedAccountUUIDProvider.setUUID(account.accountUuid)
scope.launch {
if (current != account.accountUuid) {
onAccountChanged.emit(Unit)
}
}
}
} }
override suspend fun selectAccount(account: WalletAccount) = override suspend fun selectAccount(account: WalletAccount) = selectAccount(account.sdkAccount)
withContext(Dispatchers.IO) {
selectedAccountUUIDProvider.setUUID(account.sdkAccount.accountUuid)
}
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override suspend fun importKeystoneAccount( override suspend fun importKeystoneAccount(

View File

@ -0,0 +1,7 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
class ObserveOnAccountChangedUseCase(private val accountDataSource: AccountDataSource) {
operator fun invoke() = accountDataSource.onAccountChanged
}

View File

@ -11,6 +11,8 @@ data class ReceiveAddressState(
val icon: Int, val icon: Int,
val title: StringResource, val title: StringResource,
val subtitle: StringResource, val subtitle: StringResource,
val isExpanded: Boolean,
val onClick: () -> Unit,
val isShielded: Boolean, val isShielded: Boolean,
val onCopyClicked: () -> Unit, val onCopyClicked: () -> Unit,
val onQrClicked: () -> Unit, val onQrClicked: () -> Unit,

View File

@ -21,10 +21,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
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.mutableIntStateOf
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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -85,8 +81,6 @@ private fun ReceiveContents(
items: List<ReceiveAddressState>, items: List<ReceiveAddressState>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var expandedIndex by rememberSaveable { mutableIntStateOf(0) }
Column( Column(
modifier = modifier =
modifier modifier
@ -122,8 +116,6 @@ private fun ReceiveContents(
AddressPanel( AddressPanel(
state = state, state = state,
expanded = index == expandedIndex,
onExpand = { expandedIndex = index },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
@ -133,8 +125,6 @@ private fun ReceiveContents(
@Composable @Composable
private fun AddressPanel( private fun AddressPanel(
state: ReceiveAddressState, state: ReceiveAddressState,
expanded: Boolean,
onExpand: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@ -151,7 +141,7 @@ private fun AddressPanel(
RoundedCornerShape(ZashiDimensions.Radius.radius3xl) RoundedCornerShape(ZashiDimensions.Radius.radius3xl)
) )
.clip(RoundedCornerShape(ZashiDimensions.Radius.radius3xl)) .clip(RoundedCornerShape(ZashiDimensions.Radius.radius3xl))
.clickable { onExpand() } .clickable(onClick = state.onClick)
.padding(all = ZcashTheme.dimens.spacingLarge) .padding(all = ZcashTheme.dimens.spacingLarge)
) { ) {
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
@ -192,7 +182,7 @@ private fun AddressPanel(
} }
} }
AnimatedVisibility(visible = expanded) { AnimatedVisibility(visible = state.isExpanded) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier =
@ -298,6 +288,8 @@ private fun ZashiPreview() =
onCopyClicked = {}, onCopyClicked = {},
onQrClicked = { }, onQrClicked = { },
onRequestClicked = {}, onRequestClicked = {},
isExpanded = true,
onClick = {}
), ),
ReceiveAddressState( ReceiveAddressState(
icon = R.drawable.ic_zec_round_stroke, icon = R.drawable.ic_zec_round_stroke,
@ -307,6 +299,8 @@ private fun ZashiPreview() =
onCopyClicked = {}, onCopyClicked = {},
onQrClicked = { }, onQrClicked = { },
onRequestClicked = { }, onRequestClicked = { },
isExpanded = false,
onClick = {}
) )
), ),
isLoading = false isLoading = false
@ -314,28 +308,3 @@ private fun ZashiPreview() =
zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new() zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new()
) )
} }
@PreviewScreens
@Composable
private fun KeystonePreview() =
ZcashTheme {
ReceiveView(
state =
ReceiveState(
items =
listOf(
ReceiveAddressState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_item_keystone,
title = stringRes("Keystone Address"),
subtitle = stringRes("subtitle"),
isShielded = true,
onCopyClicked = {},
onQrClicked = {},
onRequestClicked = {},
),
),
isLoading = false
),
zashiMainTopAppBarState = ZashiMainTopAppBarStateFixture.new()
)
}

View File

@ -11,39 +11,48 @@ import co.electriccoin.zcash.ui.common.model.KeystoneAccount
import co.electriccoin.zcash.ui.common.model.WalletAccount import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.model.ZashiAccount import co.electriccoin.zcash.ui.common.model.ZashiAccount
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveOnAccountChangedUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.ADDRESS_MAX_LENGTH import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.ADDRESS_MAX_LENGTH
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressState import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressState
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveState import co.electriccoin.zcash.ui.screen.receive.model.ReceiveState
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ReceiveViewModel( class ReceiveViewModel(
observeSelectedWalletAccount: ObserveSelectedWalletAccountUseCase, observeSelectedWalletAccount: ObserveSelectedWalletAccountUseCase,
observeOnAccountChanged: ObserveOnAccountChangedUseCase,
private val application: Application, private val application: Application,
private val copyToClipboard: CopyToClipboardUseCase, private val copyToClipboard: CopyToClipboardUseCase,
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
) : ViewModel() { ) : ViewModel() {
@OptIn(ExperimentalCoroutinesApi::class) private val expandedIndex = MutableStateFlow(0)
internal val state = internal val state =
observeSelectedWalletAccount.require().mapLatest { account -> combine(expandedIndex, observeSelectedWalletAccount.require()) { expandedIndex, account ->
ReceiveState( ReceiveState(
items = items =
listOfNotNull( listOfNotNull(
createAddressState( createAddressState(
account = account, account = account,
address = account.unified.address.address, address = account.unified.address.address,
type = ReceiveAddressType.Unified type = ReceiveAddressType.Unified,
isExpanded = expandedIndex == 0,
onClick = { onAddressClick(0) }
), ),
createAddressState( createAddressState(
account = account, account = account,
address = account.transparent.address.address, address = account.transparent.address.address,
type = ReceiveAddressType.Transparent type = ReceiveAddressType.Transparent,
isExpanded = expandedIndex == 1,
onClick = { onAddressClick(1) }
), ),
), ),
isLoading = false isLoading = false
@ -54,10 +63,20 @@ class ReceiveViewModel(
initialValue = ReceiveState(items = null, isLoading = true) initialValue = ReceiveState(items = null, isLoading = true)
) )
init {
viewModelScope.launch {
observeOnAccountChanged().collect {
expandedIndex.update { 0 }
}
}
}
private fun createAddressState( private fun createAddressState(
account: WalletAccount, account: WalletAccount,
address: String, address: String,
type: ReceiveAddressType type: ReceiveAddressType,
isExpanded: Boolean,
onClick: () -> Unit,
) = ReceiveAddressState( ) = ReceiveAddressState(
icon = icon =
when (account) { when (account) {
@ -94,6 +113,8 @@ class ReceiveViewModel(
}, },
onQrClicked = { onQrCodeClick(type) }, onQrClicked = { onQrCodeClick(type) },
onRequestClicked = { onRequestClick(type) }, onRequestClicked = { onRequestClick(type) },
onClick = onClick,
isExpanded = isExpanded
) )
private fun onRequestClick(addressType: ReceiveAddressType) = private fun onRequestClick(addressType: ReceiveAddressType) =
@ -101,4 +122,8 @@ class ReceiveViewModel(
private fun onQrCodeClick(addressType: ReceiveAddressType) = private fun onQrCodeClick(addressType: ReceiveAddressType) =
navigationRouter.forward("${NavigationTargets.QR_CODE}/${addressType.ordinal}") navigationRouter.forward("${NavigationTargets.QR_CODE}/${addressType.ordinal}")
private fun onAddressClick(index: Int) {
expandedIndex.update { index }
}
} }