Automatic keyboard and bottom sheet handling during navigation

This commit is contained in:
Milan Cerovsky 2025-03-31 17:07:28 +02:00
parent 696344f832
commit 33cd056570
30 changed files with 227 additions and 215 deletions

View File

@ -0,0 +1,80 @@
package co.electriccoin.zcash.ui.design
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds
@Stable
class KeyboardManager(
isOpen: Boolean,
private val softwareKeyboardController: SoftwareKeyboardController?
) {
private var targetState = MutableStateFlow(isOpen)
var isOpen by mutableStateOf(isOpen)
private set
suspend fun close() {
if (targetState.value) {
withTimeoutOrNull(.5.seconds) {
softwareKeyboardController?.hide()
targetState.filter { !it }.first()
}
}
}
fun onKeyboardOpened() {
targetState.update { true }
isOpen = true
}
fun onKeyboardClosed() {
targetState.update { false }
isOpen = false
}
}
@Composable
fun rememberKeyboardManager(): KeyboardManager {
val isKeyboardOpen by rememberKeyboardState()
val softwareKeyboardController = LocalSoftwareKeyboardController.current
val keyboardManager = remember { KeyboardManager(isKeyboardOpen, softwareKeyboardController) }
LaunchedEffect(isKeyboardOpen) {
if (isKeyboardOpen) {
keyboardManager.onKeyboardOpened()
} else {
keyboardManager.onKeyboardClosed()
}
}
return keyboardManager
}
@Composable
private fun rememberKeyboardState(): State<Boolean> {
val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
return rememberUpdatedState(isImeVisible)
}
@Suppress("CompositionLocalAllowlist")
val LocalKeyboardManager =
compositionLocalOf<KeyboardManager> {
error("Keyboard manager not provided")
}

View File

@ -0,0 +1,41 @@
package co.electriccoin.zcash.ui.design
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalMaterial3Api::class)
@Stable
class SheetStateManager {
private var sheetState: SheetState? = null
fun onSheetOpened(sheetState: SheetState) {
this.sheetState = sheetState
}
fun onSheetDisposed(sheetState: SheetState) {
if (this.sheetState == sheetState) {
this.sheetState = null
}
}
suspend fun hide() {
withTimeoutOrNull(.5.seconds) {
sheetState?.hide()
}
}
}
@Composable
fun rememberSheetStateManager() = remember { SheetStateManager() }
@Suppress("CompositionLocalAllowlist")
val LocalSheetStateManager =
compositionLocalOf<SheetStateManager> {
error("Sheet state manager not provided")
}

View File

@ -9,6 +9,10 @@ import androidx.compose.material3.RippleConfiguration
import androidx.compose.material3.RippleDefaults import androidx.compose.material3.RippleDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.rememberKeyboardManager
import co.electriccoin.zcash.ui.design.rememberSheetStateManager
import co.electriccoin.zcash.ui.design.theme.balances.LocalBalancesAvailable import co.electriccoin.zcash.ui.design.theme.balances.LocalBalancesAvailable
import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal
import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal
@ -49,7 +53,9 @@ fun ZcashTheme(
LocalZashiColors provides zashiColors, LocalZashiColors provides zashiColors,
LocalZashiTypography provides ZashiTypographyInternal, LocalZashiTypography provides ZashiTypographyInternal,
LocalRippleConfiguration provides MaterialRippleConfig, LocalRippleConfiguration provides MaterialRippleConfig,
LocalBalancesAvailable provides balancesAvailable LocalBalancesAvailable provides balancesAvailable,
LocalKeyboardManager provides rememberKeyboardManager(),
LocalSheetStateManager provides rememberSheetStateManager()
) { ) {
ProvideDimens { ProvideDimens {
MaterialTheme( MaterialTheme(

View File

@ -118,7 +118,6 @@ class MainActivity : FragmentActivity() {
} }
} }
// Note this condition needs to be kept in sync with the condition in MainContent()
SecretState.LOADING == walletViewModel.secretState.value SecretState.LOADING == walletViewModel.secretState.value
} }
} }

View File

@ -6,7 +6,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -39,6 +38,8 @@ import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW
import co.electriccoin.zcash.ui.common.compose.LocalNavController import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
import co.electriccoin.zcash.ui.common.provider.isInForeground import co.electriccoin.zcash.ui.common.provider.isInForeground
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition
@ -122,9 +123,10 @@ import org.koin.compose.koinInject
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
internal fun MainActivity.Navigation() { internal fun MainActivity.Navigation() {
val navController = LocalNavController.current val navController = LocalNavController.current
val focusManager = LocalFocusManager.current val keyboardManager = LocalKeyboardManager.current
val flexaViewModel = koinViewModel<FlexaViewModel>() val flexaViewModel = koinViewModel<FlexaViewModel>()
val navigationRouter = koinInject<NavigationRouter>() val navigationRouter = koinInject<NavigationRouter>()
val sheetStateManager = LocalSheetStateManager.current
// Helper properties for triggering the system security UI from callbacks // Helper properties for triggering the system security UI from callbacks
val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) = val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) =
@ -136,13 +138,15 @@ internal fun MainActivity.Navigation() {
remember( remember(
navController, navController,
flexaViewModel, flexaViewModel,
focusManager keyboardManager,
sheetStateManager
) { ) {
NavigatorImpl( NavigatorImpl(
activity = this@Navigation, activity = this@Navigation,
navController = navController, navController = navController,
flexaViewModel = flexaViewModel, flexaViewModel = flexaViewModel,
focusManager = focusManager keyboardManager = keyboardManager,
sheetStateManager = sheetStateManager
) )
} }

View File

@ -2,10 +2,11 @@ package co.electriccoin.zcash.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.focus.FocusManager
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavOptionsBuilder
import androidx.navigation.serialization.generateHashCode import androidx.navigation.serialization.generateHashCode
import co.electriccoin.zcash.ui.design.KeyboardManager
import co.electriccoin.zcash.ui.design.SheetStateManager
import co.electriccoin.zcash.ui.screen.ExternalUrl import co.electriccoin.zcash.ui.screen.ExternalUrl
import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
@ -15,17 +16,19 @@ import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
interface Navigator { interface Navigator {
fun executeCommand(command: NavigationCommand) suspend fun executeCommand(command: NavigationCommand)
} }
class NavigatorImpl( class NavigatorImpl(
private val activity: ComponentActivity, private val activity: ComponentActivity,
private val navController: NavHostController, private val navController: NavHostController,
private val flexaViewModel: FlexaViewModel, private val flexaViewModel: FlexaViewModel,
private val focusManager: FocusManager, private val keyboardManager: KeyboardManager,
private val sheetStateManager: SheetStateManager,
) : Navigator { ) : Navigator {
override fun executeCommand(command: NavigationCommand) { override suspend fun executeCommand(command: NavigationCommand) {
focusManager.clearFocus(true) keyboardManager.close()
sheetStateManager.hide()
when (command) { when (command) {
is NavigationCommand.Forward -> forward(command) is NavigationCommand.Forward -> forward(command)
is NavigationCommand.Replace -> replace(command) is NavigationCommand.Replace -> replace(command)

View File

@ -8,12 +8,8 @@ class ApplyTransactionFiltersUseCase(
private val transactionFilterRepository: TransactionFilterRepository, private val transactionFilterRepository: TransactionFilterRepository,
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
) { ) {
suspend operator fun invoke( operator fun invoke(filters: List<TransactionFilter>) {
filters: List<TransactionFilter>,
hideBottomSheet: suspend () -> Unit
) {
transactionFilterRepository.apply(filters) transactionFilterRepository.apply(filters)
hideBottomSheet()
navigationRouter.back() navigationRouter.back()
} }
} }

View File

@ -9,11 +9,9 @@ class CreateOrUpdateTransactionNoteUseCase(
) { ) {
suspend operator fun invoke( suspend operator fun invoke(
txId: String, txId: String,
note: String, note: String
closeBottomSheet: suspend () -> Unit
) { ) {
metadataRepository.createOrUpdateTxNote(txId, note.trim()) metadataRepository.createOrUpdateTxNote(txId, note.trim())
closeBottomSheet()
navigationRouter.back() navigationRouter.back()
} }
} }

View File

@ -7,12 +7,8 @@ class DeleteTransactionNoteUseCase(
private val metadataRepository: MetadataRepository, private val metadataRepository: MetadataRepository,
private val navigationRouter: NavigationRouter private val navigationRouter: NavigationRouter
) { ) {
suspend operator fun invoke( suspend operator fun invoke(txId: String) {
txId: String,
closeBottomSheet: suspend () -> Unit
) {
metadataRepository.deleteTxNote(txId) metadataRepository.deleteTxNote(txId)
closeBottomSheet()
navigationRouter.back() navigationRouter.back()
} }
} }

View File

@ -8,12 +8,8 @@ class SelectWalletAccountUseCase(
private val accountDataSource: AccountDataSource, private val accountDataSource: AccountDataSource,
private val navigationRouter: NavigationRouter private val navigationRouter: NavigationRouter
) { ) {
suspend operator fun invoke( suspend operator fun invoke(account: WalletAccount) {
account: WalletAccount,
hideBottomSheet: suspend () -> Unit
) {
accountDataSource.selectAccount(account) accountDataSource.selectAccount(account)
hideBottomSheet()
navigationRouter.back() navigationRouter.back()
} }
} }

View File

@ -4,12 +4,14 @@ import android.view.WindowManager
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
import co.electriccoin.zcash.ui.screen.accountlist.view.AccountListView import co.electriccoin.zcash.ui.screen.accountlist.view.AccountListView
import co.electriccoin.zcash.ui.screen.accountlist.viewmodel.AccountListViewModel import co.electriccoin.zcash.ui.screen.accountlist.viewmodel.AccountListViewModel
@ -22,6 +24,13 @@ fun AndroidAccountList() {
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val sheetManager = LocalSheetStateManager.current
DisposableEffect(sheetState) {
sheetManager.onSheetOpened(sheetState)
onDispose {
sheetManager.onSheetDisposed(sheetState)
}
}
val parent = LocalView.current.parent val parent = LocalView.current.parent
@ -43,13 +52,6 @@ fun AndroidAccountList() {
sheetState.show() sheetState.show()
} }
LaunchedEffect(Unit) {
viewModel.hideBottomSheetRequest.collect {
sheetState.hide()
state?.onBottomSheetHidden?.invoke()
}
}
BackHandler { BackHandler {
state?.onBack?.invoke() state?.onBack?.invoke()
} }

View File

@ -8,7 +8,6 @@ import co.electriccoin.zcash.ui.design.util.StringResource
data class AccountListState( data class AccountListState(
val items: List<AccountListItem>?, val items: List<AccountListItem>?,
val isLoading: Boolean, val isLoading: Boolean,
val onBottomSheetHidden: () -> Unit,
val addWalletButton: ButtonState?, val addWalletButton: ButtonState?,
val onBack: () -> Unit, val onBack: () -> Unit,
) )

View File

@ -270,7 +270,6 @@ private fun Preview() =
) )
), ),
isLoading = false, isLoading = false,
onBottomSheetHidden = {},
onBack = {}, onBack = {},
addWalletButton = ButtonState(stringRes("Connect Hardware Wallet")) addWalletButton = ButtonState(stringRes("Connect Hardware Wallet"))
), ),
@ -315,7 +314,6 @@ private fun HardwareWalletAddedPreview() =
), ),
), ),
isLoading = false, isLoading = false,
onBottomSheetHidden = {},
onBack = {}, onBack = {},
addWalletButton = null addWalletButton = null
), ),

View File

@ -19,10 +19,8 @@ import co.electriccoin.zcash.ui.screen.accountlist.model.AccountListState
import co.electriccoin.zcash.ui.screen.accountlist.model.ZashiAccountListItemState import co.electriccoin.zcash.ui.screen.accountlist.model.ZashiAccountListItemState
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.connectkeystone.ConnectKeystone import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -32,10 +30,6 @@ class AccountListViewModel(
private val selectWalletAccount: SelectWalletAccountUseCase, private val selectWalletAccount: SelectWalletAccountUseCase,
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
) : ViewModel() { ) : ViewModel() {
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
@Suppress("SpreadOperator") @Suppress("SpreadOperator")
val state = val state =
getWalletAccounts.observe().map { accounts -> getWalletAccounts.observe().map { accounts ->
@ -77,7 +71,6 @@ class AccountListViewModel(
AccountListState( AccountListState(
items = items, items = items,
isLoading = accounts == null, isLoading = accounts == null,
onBottomSheetHidden = ::onBottomSheetHidden,
onBack = ::onBack, onBack = ::onBack,
addWalletButton = addWalletButton =
ButtonState( ButtonState(
@ -94,35 +87,14 @@ class AccountListViewModel(
) )
private fun onShowKeystonePromoClicked() = private fun onShowKeystonePromoClicked() =
viewModelScope.launch { navigationRouter.replace(ExternalUrl("https://keyst.one/shop/products/keystone-3-pro?discount=Zashi"))
hideBottomSheet()
navigationRouter.replace(ExternalUrl("https://keyst.one/shop/products/keystone-3-pro?discount=Zashi"))
}
private suspend fun hideBottomSheet() {
hideBottomSheetRequest.emit(Unit)
bottomSheetHiddenResponse.first()
}
private fun onBottomSheetHidden() =
viewModelScope.launch {
bottomSheetHiddenResponse.emit(Unit)
}
private fun onAccountClicked(account: WalletAccount) = private fun onAccountClicked(account: WalletAccount) =
viewModelScope.launch { viewModelScope.launch {
selectWalletAccount(account) { hideBottomSheet() } selectWalletAccount(account)
} }
private fun onAddWalletButtonClicked() = private fun onAddWalletButtonClicked() = navigationRouter.forward(ConnectKeystone)
viewModelScope.launch {
hideBottomSheet()
navigationRouter.forward(ConnectKeystone)
}
private fun onBack() = private fun onBack() = navigationRouter.back()
viewModelScope.launch {
hideBottomSheet()
navigationRouter.back()
}
} }

View File

@ -4,12 +4,14 @@ import android.view.WindowManager
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@ -19,6 +21,13 @@ import org.koin.core.parameter.parametersOf
@Composable @Composable
fun AndroidDialogIntegrations() { fun AndroidDialogIntegrations() {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val sheetManager = LocalSheetStateManager.current
DisposableEffect(sheetState) {
sheetManager.onSheetOpened(sheetState)
onDispose {
sheetManager.onSheetDisposed(sheetState)
}
}
val parent = LocalView.current.parent val parent = LocalView.current.parent
val viewModel = koinViewModel<IntegrationsViewModel> { parametersOf(true) } val viewModel = koinViewModel<IntegrationsViewModel> { parametersOf(true) }
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
@ -44,13 +53,6 @@ fun AndroidDialogIntegrations() {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
sheetState.show() sheetState.show()
} }
LaunchedEffect(Unit) {
viewModel.hideBottomSheetRequest.collect {
sheetState.hide()
state?.onBottomSheetHidden?.invoke()
}
}
} }
} }

View File

@ -132,7 +132,6 @@ private fun IntegrationSettings() =
onClick = {} onClick = {}
), ),
), ),
onBottomSheetHidden = {}
), ),
) )
} }

View File

@ -8,5 +8,4 @@ data class IntegrationsState(
val disabledInfo: StringResource?, val disabledInfo: StringResource?,
val onBack: () -> Unit, val onBack: () -> Unit,
val items: ImmutableList<ZashiListItemState>, val items: ImmutableList<ZashiListItemState>,
val onBottomSheetHidden: () -> Unit,
) )

View File

@ -197,7 +197,6 @@ private fun IntegrationSettings() =
onClick = {} onClick = {}
), ),
), ),
onBottomSheetHidden = {}
), ),
topAppBarSubTitleState = TopAppBarSubTitleState.None, topAppBarSubTitleState = TopAppBarSubTitleState.None,
) )

View File

@ -25,11 +25,9 @@ import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
import co.electriccoin.zcash.ui.screen.flexa.Flexa import co.electriccoin.zcash.ui.screen.flexa.Flexa
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -45,10 +43,6 @@ class IntegrationsViewModel(
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
private val navigateToCoinbase: NavigateToCoinbaseUseCase, private val navigateToCoinbase: NavigateToCoinbaseUseCase,
) : ViewModel() { ) : ViewModel() {
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
private val isRestoring = getWalletRestoringState.observe().map { it == WalletRestoringState.RESTORING } private val isRestoring = getWalletRestoringState.observe().map { it == WalletRestoringState.RESTORING }
val state = val state =
@ -123,43 +117,25 @@ class IntegrationsViewModel(
onClick = ::onConnectKeystoneClick onClick = ::onConnectKeystoneClick
).takeIf { keystoneStatus != UNAVAILABLE }, ).takeIf { keystoneStatus != UNAVAILABLE },
).toImmutableList(), ).toImmutableList(),
onBottomSheetHidden = ::onBottomSheetHidden
) )
private fun onBack() = navigationRouter.back() private fun onBack() = navigationRouter.back()
private suspend fun hideBottomSheet() {
if (isDialog) {
hideBottomSheetRequest.emit(Unit)
bottomSheetHiddenResponse.first()
}
}
private fun onBottomSheetHidden() =
viewModelScope.launch {
bottomSheetHiddenResponse.emit(Unit)
}
private fun onBuyWithCoinbaseClicked() = private fun onBuyWithCoinbaseClicked() =
viewModelScope.launch { viewModelScope.launch {
hideBottomSheet()
navigateToCoinbase(isDialog) navigateToCoinbase(isDialog)
} }
private fun onConnectKeystoneClick() = private fun onConnectKeystoneClick() =
viewModelScope.launch { viewModelScope.launch {
hideBottomSheet()
navigationRouter.replace(ConnectKeystone) navigationRouter.replace(ConnectKeystone)
} }
private fun onFlexaClicked() = private fun onFlexaClicked() {
viewModelScope.launch { if (isDialog) {
if (isDialog) { navigationRouter.replace(Flexa)
hideBottomSheet() } else {
navigationRouter.replace(Flexa) navigationRouter.forward(Flexa)
} else {
hideBottomSheet()
navigationRouter.forward(Flexa)
}
} }
}
} }

View File

@ -6,7 +6,6 @@ import android.content.Context
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -27,6 +26,8 @@ import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition
@ -51,19 +52,23 @@ fun MainActivity.OnboardingNavigation() {
val activity = LocalActivity.current val activity = LocalActivity.current
val navigationRouter = koinInject<NavigationRouter>() val navigationRouter = koinInject<NavigationRouter>()
val navController = LocalNavController.current val navController = LocalNavController.current
val focusManager = LocalFocusManager.current val keyboardManager = LocalKeyboardManager.current
val flexaViewModel = koinViewModel<FlexaViewModel>() val flexaViewModel = koinViewModel<FlexaViewModel>()
val sheetStateManager = LocalSheetStateManager.current
val navigator: Navigator = val navigator: Navigator =
remember( remember(
navController, navController,
flexaViewModel, flexaViewModel,
focusManager keyboardManager,
sheetStateManager
) { ) {
NavigatorImpl( NavigatorImpl(
activity = this@OnboardingNavigation, activity = this@OnboardingNavigation,
navController = navController, navController = navController,
flexaViewModel = flexaViewModel, flexaViewModel = flexaViewModel,
focusManager = focusManager keyboardManager = keyboardManager,
sheetStateManager = sheetStateManager
) )
} }

View File

@ -3,12 +3,14 @@ package co.electriccoin.zcash.ui.screen.restore.info
import android.view.WindowManager import android.view.WindowManager
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -27,6 +29,14 @@ fun AndroidSeedInfo() {
} }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val sheetManager = LocalSheetStateManager.current
DisposableEffect(sheetState) {
sheetManager.onSheetOpened(sheetState)
onDispose {
sheetManager.onSheetDisposed(sheetState)
}
}
SeedInfoView( SeedInfoView(
sheetState = sheetState, sheetState = sheetState,
@ -34,14 +44,14 @@ fun AndroidSeedInfo() {
SeedInfoState( SeedInfoState(
onBack = { onBack = {
scope.launch { scope.launch {
sheetState.hide() // sheetState.hide()
navigationRouter.back() navigationRouter.back()
} }
} }
), ),
onDismissRequest = { onDismissRequest = {
scope.launch { scope.launch {
sheetState.hide() // sheetState.hide()
navigationRouter.back() navigationRouter.back()
} }
} }

View File

@ -8,11 +8,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -25,17 +23,14 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
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.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -44,6 +39,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarTags import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarTags
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.IconButtonState import co.electriccoin.zcash.ui.design.component.IconButtonState
@ -76,13 +72,12 @@ fun RestoreSeedView(
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
var wasKeyboardOpen by remember { mutableStateOf(false) } var wasKeyboardOpen by remember { mutableStateOf(false) }
val isKeyboardOpen by rememberKeyboardState() val keyboardManager = LocalKeyboardManager.current
val isKeyboardOpen = keyboardManager.isOpen
LaunchedEffect(isKeyboardOpen) { LaunchedEffect(isKeyboardOpen) {
if (wasKeyboardOpen && !isKeyboardOpen) { if (wasKeyboardOpen && !isKeyboardOpen) {
focusManager.clearFocus(true) focusManager.clearFocus(true)
} }
wasKeyboardOpen = isKeyboardOpen wasKeyboardOpen = isKeyboardOpen
} }
@ -278,12 +273,6 @@ private fun getFilteredSuggestions(
} }
} }
@Composable
private fun rememberKeyboardState(): State<Boolean> {
val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
return rememberUpdatedState(isImeVisible)
}
@PreviewScreens @PreviewScreens
@Composable @Composable
private fun Preview() = private fun Preview() =

View File

@ -4,12 +4,14 @@ import android.view.WindowManager
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
import co.electriccoin.zcash.ui.screen.transactionfilters.view.TransactionFiltersView import co.electriccoin.zcash.ui.screen.transactionfilters.view.TransactionFiltersView
import co.electriccoin.zcash.ui.screen.transactionfilters.viewmodel.TransactionFiltersViewModel import co.electriccoin.zcash.ui.screen.transactionfilters.viewmodel.TransactionFiltersViewModel
@ -22,6 +24,13 @@ fun AndroidTransactionFiltersList() {
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val sheetManager = LocalSheetStateManager.current
DisposableEffect(sheetState) {
sheetManager.onSheetOpened(sheetState)
onDispose {
sheetManager.onSheetDisposed(sheetState)
}
}
val parent = LocalView.current.parent val parent = LocalView.current.parent
@ -40,13 +49,6 @@ fun AndroidTransactionFiltersList() {
sheetState.show() sheetState.show()
} }
LaunchedEffect(Unit) {
viewModel.hideBottomSheetRequest.collect {
sheetState.hide()
state?.onBottomSheetHidden?.invoke()
}
}
BackHandler(state != null) { BackHandler(state != null) {
state?.onBack?.invoke() state?.onBack?.invoke()
} }

View File

@ -55,14 +55,12 @@ object TransactionFiltersStateFixture {
fun new( fun new(
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onBottomSheetHidden: () -> Unit = {},
filters: List<TransactionFilterState> = FILTERS, filters: List<TransactionFilterState> = FILTERS,
primaryButtonState: ButtonState = PRIMARY_BUTTON_STATE, primaryButtonState: ButtonState = PRIMARY_BUTTON_STATE,
secondaryButtonState: ButtonState = SECONDARY_BUTTON_STATE secondaryButtonState: ButtonState = SECONDARY_BUTTON_STATE
) = TransactionFiltersState( ) = TransactionFiltersState(
filters = filters, filters = filters,
onBack = onBack, onBack = onBack,
onBottomSheetHidden = onBottomSheetHidden,
primaryButton = primaryButtonState, primaryButton = primaryButtonState,
secondaryButton = secondaryButtonState secondaryButton = secondaryButtonState
) )

View File

@ -6,7 +6,6 @@ import co.electriccoin.zcash.ui.design.util.StringResource
data class TransactionFiltersState( data class TransactionFiltersState(
val filters: List<TransactionFilterState>, val filters: List<TransactionFilterState>,
val onBack: () -> Unit, val onBack: () -> Unit,
val onBottomSheetHidden: () -> Unit,
val primaryButton: ButtonState, val primaryButton: ButtonState,
val secondaryButton: ButtonState val secondaryButton: ButtonState
) )

View File

@ -19,26 +19,19 @@ import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.transactionfilters.model.TransactionFilterState import co.electriccoin.zcash.ui.screen.transactionfilters.model.TransactionFilterState
import co.electriccoin.zcash.ui.screen.transactionfilters.model.TransactionFiltersState import co.electriccoin.zcash.ui.screen.transactionfilters.model.TransactionFiltersState
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
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.first
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class TransactionFiltersViewModel( internal class TransactionFiltersViewModel(
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
getTransactionFilters: GetTransactionFiltersUseCase, getTransactionFilters: GetTransactionFiltersUseCase,
private val applyTransactionFilters: ApplyTransactionFiltersUseCase, private val applyTransactionFilters: ApplyTransactionFiltersUseCase,
) : ViewModel() { ) : ViewModel() {
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
private val selectedFilters = MutableStateFlow(getTransactionFilters()) private val selectedFilters = MutableStateFlow(getTransactionFilters())
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -87,7 +80,6 @@ internal class TransactionFiltersViewModel(
} }
}, },
onBack = ::onBack, onBack = ::onBack,
onBottomSheetHidden = ::onBottomSheetHidden,
primaryButton = primaryButton =
ButtonState( ButtonState(
text = stringRes(R.string.transaction_filters_btn_apply), text = stringRes(R.string.transaction_filters_btn_apply),
@ -100,9 +92,9 @@ internal class TransactionFiltersViewModel(
), ),
) )
}.stateIn( }.stateIn(
viewModelScope, scope = viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null initialValue = null
) )
private fun onTransactionFilterClicked(filter: TransactionFilter) { private fun onTransactionFilterClicked(filter: TransactionFilter) {
@ -115,29 +107,9 @@ internal class TransactionFiltersViewModel(
} }
} }
private suspend fun hideBottomSheet() { private fun onBack() = navigationRouter.back()
hideBottomSheetRequest.emit(Unit)
bottomSheetHiddenResponse.first()
}
private fun onBottomSheetHidden() = private fun onApplyTransactionFiltersClick() = applyTransactionFilters(selectedFilters.value)
viewModelScope.launch {
bottomSheetHiddenResponse.emit(Unit)
}
private fun onBack() = private fun onResetTransactionFiltersClick() = selectedFilters.update { emptyList() }
viewModelScope.launch {
hideBottomSheet()
navigationRouter.back()
}
private fun onApplyTransactionFiltersClick() =
viewModelScope.launch {
applyTransactionFilters(selectedFilters.value) { hideBottomSheet() }
}
private fun onResetTransactionFiltersClick() =
viewModelScope.launch {
selectedFilters.update { emptyList() }
}
} }

View File

@ -5,6 +5,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetValue.Expanded import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -12,6 +13,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
import co.electriccoin.zcash.ui.screen.transactionnote.view.TransactionNoteView import co.electriccoin.zcash.ui.screen.transactionnote.view.TransactionNoteView
import co.electriccoin.zcash.ui.screen.transactionnote.viewmodel.TransactionNoteViewModel import co.electriccoin.zcash.ui.screen.transactionnote.viewmodel.TransactionNoteViewModel
@ -26,7 +28,13 @@ fun AndroidTransactionNote(transactionNote: TransactionNote) {
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val sheetManager = LocalSheetStateManager.current
DisposableEffect(sheetState) {
sheetManager.onSheetOpened(sheetState)
onDispose {
sheetManager.onSheetDisposed(sheetState)
}
}
val parent = LocalView.current.parent val parent = LocalView.current.parent
SideEffect { SideEffect {
@ -52,13 +60,6 @@ fun AndroidTransactionNote(transactionNote: TransactionNote) {
} }
} }
LaunchedEffect(Unit) {
viewModel.hideBottomSheetRequest.collect {
sheetState.hide()
state.onBottomSheetHidden()
}
}
BackHandler { BackHandler {
state.onBack() state.onBack()
} }

View File

@ -7,7 +7,6 @@ import co.electriccoin.zcash.ui.design.util.StyledStringResource
data class TransactionNoteState( data class TransactionNoteState(
val onBack: () -> Unit, val onBack: () -> Unit,
val onBottomSheetHidden: () -> Unit,
val title: StringResource, val title: StringResource,
val note: TextFieldState, val note: TextFieldState,
val noteCharacters: StyledStringResource, val noteCharacters: StyledStringResource,

View File

@ -137,7 +137,6 @@ private fun Preview() =
state = state =
TransactionNoteState( TransactionNoteState(
onBack = {}, onBack = {},
onBottomSheetHidden = {},
title = stringRes("Title"), title = stringRes("Title"),
note = TextFieldState(stringRes("")) {}, note = TextFieldState(stringRes("")) {},
noteCharacters = noteCharacters =

View File

@ -15,13 +15,11 @@ import co.electriccoin.zcash.ui.design.util.StyledStringResource
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.transactionnote.TransactionNote import co.electriccoin.zcash.ui.screen.transactionnote.TransactionNote
import co.electriccoin.zcash.ui.screen.transactionnote.model.TransactionNoteState import co.electriccoin.zcash.ui.screen.transactionnote.model.TransactionNoteState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
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.first
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -33,10 +31,6 @@ internal class TransactionNoteViewModel(
private val createOrUpdateTransactionNote: CreateOrUpdateTransactionNoteUseCase, private val createOrUpdateTransactionNote: CreateOrUpdateTransactionNoteUseCase,
private val deleteTransactionNote: DeleteTransactionNoteUseCase private val deleteTransactionNote: DeleteTransactionNoteUseCase
) : ViewModel() { ) : ViewModel() {
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
private val noteText = MutableStateFlow("") private val noteText = MutableStateFlow("")
private val foundNote = MutableStateFlow<String?>(null) private val foundNote = MutableStateFlow<String?>(null)
@ -54,12 +48,9 @@ internal class TransactionNoteViewModel(
foundNote: String? foundNote: String?
): TransactionNoteState { ): TransactionNoteState {
val noteTextNormalized = noteText.trim() val noteTextNormalized = noteText.trim()
val isNoteTextTooLong = noteText.length > MAX_NOTE_LENGTH val isNoteTextTooLong = noteText.length > MAX_NOTE_LENGTH
return TransactionNoteState( return TransactionNoteState(
onBack = ::onBack, onBack = ::onBack,
onBottomSheetHidden = ::onBottomSheetHidden,
title = title =
if (foundNote == null) { if (foundNote == null) {
stringRes(R.string.transaction_note_add_note_title) stringRes(R.string.transaction_note_add_note_title)
@ -107,37 +98,19 @@ internal class TransactionNoteViewModel(
private fun onAddOrUpdateNoteClick() = private fun onAddOrUpdateNoteClick() =
viewModelScope.launch { viewModelScope.launch {
createOrUpdateTransactionNote(txId = transactionNote.txId, note = noteText.value) { createOrUpdateTransactionNote(txId = transactionNote.txId, note = noteText.value)
hideBottomSheet()
}
} }
private fun onDeleteNoteClick() = private fun onDeleteNoteClick() =
viewModelScope.launch { viewModelScope.launch {
deleteTransactionNote(transactionNote.txId) { deleteTransactionNote(transactionNote.txId)
hideBottomSheet()
}
} }
private fun onNoteTextChanged(newValue: String) { private fun onNoteTextChanged(newValue: String) {
noteText.update { newValue } noteText.update { newValue }
} }
private suspend fun hideBottomSheet() { private fun onBack() = navigationRouter.back()
hideBottomSheetRequest.emit(Unit)
bottomSheetHiddenResponse.first()
}
private fun onBottomSheetHidden() =
viewModelScope.launch {
bottomSheetHiddenResponse.emit(Unit)
}
private fun onBack() =
viewModelScope.launch {
hideBottomSheet()
navigationRouter.back()
}
} }
private const val MAX_NOTE_LENGTH = 90 private const val MAX_NOTE_LENGTH = 90