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.runtime.Composable
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.colors.DarkZashiColorsInternal
import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal
@ -49,7 +53,9 @@ fun ZcashTheme(
LocalZashiColors provides zashiColors,
LocalZashiTypography provides ZashiTypographyInternal,
LocalRippleConfiguration provides MaterialRippleConfig,
LocalBalancesAvailable provides balancesAvailable
LocalBalancesAvailable provides balancesAvailable,
LocalKeyboardManager provides rememberKeyboardManager(),
LocalSheetStateManager provides rememberSheetStateManager()
) {
ProvideDimens {
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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -270,7 +270,6 @@ private fun Preview() =
)
),
isLoading = false,
onBottomSheetHidden = {},
onBack = {},
addWalletButton = ButtonState(stringRes("Connect Hardware Wallet"))
),
@ -315,7 +314,6 @@ private fun HardwareWalletAddedPreview() =
),
),
isLoading = false,
onBottomSheetHidden = {},
onBack = {},
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.addressbook.viewmodel.ADDRESS_MAX_LENGTH
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -32,10 +30,6 @@ class AccountListViewModel(
private val selectWalletAccount: SelectWalletAccountUseCase,
private val navigationRouter: NavigationRouter,
) : ViewModel() {
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
@Suppress("SpreadOperator")
val state =
getWalletAccounts.observe().map { accounts ->
@ -77,7 +71,6 @@ class AccountListViewModel(
AccountListState(
items = items,
isLoading = accounts == null,
onBottomSheetHidden = ::onBottomSheetHidden,
onBack = ::onBack,
addWalletButton =
ButtonState(
@ -94,35 +87,14 @@ class AccountListViewModel(
)
private fun onShowKeystonePromoClicked() =
viewModelScope.launch {
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) =
viewModelScope.launch {
selectWalletAccount(account) { hideBottomSheet() }
selectWalletAccount(account)
}
private fun onAddWalletButtonClicked() =
viewModelScope.launch {
hideBottomSheet()
navigationRouter.forward(ConnectKeystone)
}
private fun onAddWalletButtonClicked() = navigationRouter.forward(ConnectKeystone)
private fun onBack() =
viewModelScope.launch {
hideBottomSheet()
navigationRouter.back()
}
private fun onBack() = navigationRouter.back()
}

View File

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

View File

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

View File

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

View File

@ -197,7 +197,6 @@ private fun IntegrationSettings() =
onClick = {}
),
),
onBottomSheetHidden = {}
),
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.flexa.Flexa
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -45,10 +43,6 @@ class IntegrationsViewModel(
private val navigationRouter: NavigationRouter,
private val navigateToCoinbase: NavigateToCoinbaseUseCase,
) : ViewModel() {
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
private val isRestoring = getWalletRestoringState.observe().map { it == WalletRestoringState.RESTORING }
val state =
@ -123,42 +117,24 @@ class IntegrationsViewModel(
onClick = ::onConnectKeystoneClick
).takeIf { keystoneStatus != UNAVAILABLE },
).toImmutableList(),
onBottomSheetHidden = ::onBottomSheetHidden
)
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() =
viewModelScope.launch {
hideBottomSheet()
navigateToCoinbase(isDialog)
}
private fun onConnectKeystoneClick() =
viewModelScope.launch {
hideBottomSheet()
navigationRouter.replace(ConnectKeystone)
}
private fun onFlexaClicked() =
viewModelScope.launch {
private fun onFlexaClicked() {
if (isDialog) {
hideBottomSheet()
navigationRouter.replace(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.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.compose.NavHost
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.WalletRestoringState
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.exitTransition
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition
@ -51,19 +52,23 @@ fun MainActivity.OnboardingNavigation() {
val activity = LocalActivity.current
val navigationRouter = koinInject<NavigationRouter>()
val navController = LocalNavController.current
val focusManager = LocalFocusManager.current
val keyboardManager = LocalKeyboardManager.current
val flexaViewModel = koinViewModel<FlexaViewModel>()
val sheetStateManager = LocalSheetStateManager.current
val navigator: Navigator =
remember(
navController,
flexaViewModel,
focusManager
keyboardManager,
sheetStateManager
) {
NavigatorImpl(
activity = this@OnboardingNavigation,
navController = navController,
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 androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@ -27,6 +29,14 @@ fun AndroidSeedInfo() {
}
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val sheetManager = LocalSheetStateManager.current
DisposableEffect(sheetState) {
sheetManager.onSheetOpened(sheetState)
onDispose {
sheetManager.onSheetDisposed(sheetState)
}
}
SeedInfoView(
sheetState = sheetState,
@ -34,14 +44,14 @@ fun AndroidSeedInfo() {
SeedInfoState(
onBack = {
scope.launch {
sheetState.hide()
// sheetState.hide()
navigationRouter.back()
}
}
),
onDismissRequest = {
scope.launch {
sheetState.hide()
// sheetState.hide()
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.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -25,17 +23,14 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
@ -44,6 +39,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R
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.ButtonState
import co.electriccoin.zcash.ui.design.component.IconButtonState
@ -76,13 +72,12 @@ fun RestoreSeedView(
val focusManager = LocalFocusManager.current
var wasKeyboardOpen by remember { mutableStateOf(false) }
val isKeyboardOpen by rememberKeyboardState()
val keyboardManager = LocalKeyboardManager.current
val isKeyboardOpen = keyboardManager.isOpen
LaunchedEffect(isKeyboardOpen) {
if (wasKeyboardOpen && !isKeyboardOpen) {
focusManager.clearFocus(true)
}
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
@Composable
private fun Preview() =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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