[#584 ]Adopt Twitter's Compose Detekt rules
Twitter had a set of Compose rules, which have been forked into Detekt, Ktlint, and Android Lint implementations. I see ktlint as mostly being formatting, Detekt being more for multiplatform static analysis, and Android Lint for Android-specific issues. Android lint is slow, so I didn’t want to use it as the first choice. I went with the Detekt implementation which can be useful if we leverage multiplatform for our Composables in the future.
This commit is contained in:
parent
6d01f210fe
commit
4acd5d3593
|
@ -5,6 +5,10 @@ plugins {
|
||||||
id("io.gitlab.arturbosch.detekt")
|
id("io.gitlab.arturbosch.detekt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
detektPlugins("io.nlopez.compose.rules:detekt:${project.property("DETEKT_COMPOSE_RULES_VERSION")}")
|
||||||
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
register("detektAll", Detekt::class) {
|
register("detektAll", Detekt::class) {
|
||||||
parallel = true
|
parallel = true
|
||||||
|
|
|
@ -3,6 +3,7 @@ package co.electriccoin.zcash.configuration.api
|
||||||
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
|
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
|
||||||
import co.electriccoin.zcash.configuration.model.map.Configuration
|
import co.electriccoin.zcash.configuration.model.map.Configuration
|
||||||
import kotlinx.collections.immutable.PersistentList
|
import kotlinx.collections.immutable.PersistentList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
@ -16,7 +17,7 @@ class MergingConfigurationProvider(private val configurationProviders: Persisten
|
||||||
|
|
||||||
override fun getConfigurationFlow(): Flow<Configuration> {
|
override fun getConfigurationFlow(): Flow<Configuration> {
|
||||||
return if (configurationProviders.isEmpty()) {
|
return if (configurationProviders.isEmpty()) {
|
||||||
flowOf(MergingConfiguration(emptyList<Configuration>().toPersistentList()))
|
flowOf(MergingConfiguration(persistentListOf<Configuration>()))
|
||||||
} else {
|
} else {
|
||||||
combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
|
combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
|
||||||
MergingConfiguration(configurations.toList().toPersistentList())
|
MergingConfiguration(configurations.toList().toPersistentList())
|
||||||
|
|
|
@ -102,6 +102,7 @@ ANDROID_NDK_VERSION=23.0.7599858
|
||||||
|
|
||||||
ANDROID_GRADLE_PLUGIN_VERSION=7.4.0
|
ANDROID_GRADLE_PLUGIN_VERSION=7.4.0
|
||||||
DETEKT_VERSION=1.22.0
|
DETEKT_VERSION=1.22.0
|
||||||
|
DETEKT_COMPOSE_RULES_VERSION=0.1.2
|
||||||
EMULATOR_WTF_GRADLE_PLUGIN_VERSION=0.0.15
|
EMULATOR_WTF_GRADLE_PLUGIN_VERSION=0.0.15
|
||||||
FIREBASE_CRASHLYTICS_BUILD_TOOLS_VERSION=2.9.2
|
FIREBASE_CRASHLYTICS_BUILD_TOOLS_VERSION=2.9.2
|
||||||
FLANK_VERSION=23.01.0
|
FLANK_VERSION=23.01.0
|
||||||
|
|
|
@ -29,3 +29,7 @@ style:
|
||||||
excludes: [ '**/*.kts' ]
|
excludes: [ '**/*.kts' ]
|
||||||
WildcardImport:
|
WildcardImport:
|
||||||
active: false
|
active: false
|
||||||
|
|
||||||
|
Compose:
|
||||||
|
ModifierMissing:
|
||||||
|
active: false
|
||||||
|
|
|
@ -35,6 +35,7 @@ dependencies {
|
||||||
implementation(libs.kotlin.stdlib)
|
implementation(libs.kotlin.stdlib)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
api(libs.kotlinx.immutable)
|
||||||
implementation(projects.spackleAndroidLib)
|
implementation(projects.spackleAndroidLib)
|
||||||
|
|
||||||
androidTestImplementation(libs.bundles.androidx.test)
|
androidTestImplementation(libs.bundles.androidx.test)
|
||||||
|
|
|
@ -162,12 +162,12 @@ fun DangerousButton(
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
@Composable
|
@Composable
|
||||||
fun TimedButton(
|
fun TimedButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
content: @Composable (RowScope.() -> Unit),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
duration: Duration = 5.seconds,
|
duration: Duration = 5.seconds,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||||
onClick: () -> Unit,
|
|
||||||
content: @Composable RowScope.() -> Unit
|
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(interactionSource) {
|
LaunchedEffect(interactionSource) {
|
||||||
var action: Job? = null
|
var action: Job? = null
|
||||||
|
|
|
@ -8,12 +8,13 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import co.electriccoin.zcash.spackle.model.Index
|
import co.electriccoin.zcash.spackle.model.Index
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
// Note: Row size should probably change for landscape layouts
|
// Note: Row size should probably change for landscape layouts
|
||||||
const val CHIP_GRID_ROW_SIZE = 3
|
const val CHIP_GRID_ROW_SIZE = 3
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChipGrid(wordList: List<String>) {
|
fun ChipGrid(wordList: ImmutableList<String>) {
|
||||||
Column(Modifier.testTag(CommonTag.CHIP_LAYOUT)) {
|
Column(Modifier.testTag(CommonTag.CHIP_LAYOUT)) {
|
||||||
wordList.chunked(CHIP_GRID_ROW_SIZE).forEachIndexed { chunkIndex, chunk ->
|
wordList.chunked(CHIP_GRID_ROW_SIZE).forEachIndexed { chunkIndex, chunk ->
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
|
|
@ -46,8 +46,8 @@ fun Body(
|
||||||
@Composable
|
@Composable
|
||||||
fun Small(
|
fun Small(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
textAlign: TextAlign,
|
||||||
textAlign: TextAlign
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
|
|
|
@ -173,6 +173,7 @@ internal val LightExtendedColorPalette = ExtendedColors(
|
||||||
reference = Light.reference
|
reference = Light.reference
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Suppress("CompositionLocalAllowlist")
|
||||||
internal val LocalExtendedColors = staticCompositionLocalOf {
|
internal val LocalExtendedColors = staticCompositionLocalOf {
|
||||||
ExtendedColors(
|
ExtendedColors(
|
||||||
surfaceEnd = Color.Unspecified,
|
surfaceEnd = Color.Unspecified,
|
||||||
|
|
|
@ -70,6 +70,7 @@ data class ExtendedTypography(
|
||||||
val zecBalance: TextStyle
|
val zecBalance: TextStyle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Suppress("CompositionLocalAllowlist")
|
||||||
val LocalExtendedTypography = staticCompositionLocalOf {
|
val LocalExtendedTypography = staticCompositionLocalOf {
|
||||||
ExtendedTypography(
|
ExtendedTypography(
|
||||||
chipIndex = Typography.bodyLarge.copy(
|
chipIndex = Typography.bodyLarge.copy(
|
||||||
|
|
|
@ -34,7 +34,7 @@ class ScanViewIntegrationTest : UiTestPrerequisites() {
|
||||||
val restorationTester = StateRestorationTester(composeTestRule)
|
val restorationTester = StateRestorationTester(composeTestRule)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(testSetup.getScanState(), ScanState.Permission)
|
assertEquals(testSetup.getScanState(), ScanState.Permission)
|
||||||
|
@ -54,7 +54,7 @@ class ScanViewIntegrationTest : UiTestPrerequisites() {
|
||||||
val restorationTester = StateRestorationTester(composeTestRule)
|
val restorationTester = StateRestorationTester(composeTestRule)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissionPositiveButtonUiObject = getPermissionPositiveButtonUiObject()
|
val permissionPositiveButtonUiObject = getPermissionPositiveButtonUiObject()
|
||||||
|
@ -78,7 +78,7 @@ class ScanViewIntegrationTest : UiTestPrerequisites() {
|
||||||
val restorationTester = StateRestorationTester(composeTestRule)
|
val restorationTester = StateRestorationTester(composeTestRule)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
testSetup.grantPermission()
|
testSetup.grantPermission()
|
||||||
|
|
|
@ -46,7 +46,7 @@ class ScanViewTestSetup(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getDefaultContent() {
|
fun DefaultContent() {
|
||||||
Scan(
|
Scan(
|
||||||
snackbarHostState = SnackbarHostState(),
|
snackbarHostState = SnackbarHostState(),
|
||||||
onBack = {},
|
onBack = {},
|
||||||
|
@ -63,7 +63,7 @@ class ScanViewTestSetup(
|
||||||
fun setDefaultContent() {
|
fun setDefaultContent() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
getDefaultContent()
|
DefaultContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@ dependencies {
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.coroutines.guava)
|
implementation(libs.kotlinx.coroutines.guava)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
|
implementation(libs.kotlinx.immutable)
|
||||||
implementation(libs.zcash.sdk)
|
implementation(libs.zcash.sdk)
|
||||||
implementation(libs.zcash.sdk.incubator)
|
implementation(libs.zcash.sdk.incubator)
|
||||||
implementation(libs.zcash.bip39)
|
implementation(libs.zcash.bip39)
|
||||||
|
|
|
@ -50,7 +50,7 @@ class BackupIntegrationTest : UiTestPrerequisites() {
|
||||||
val testSetup = newTestSetup(BackupStage.EducationOverview)
|
val testSetup = newTestSetup(BackupStage.EducationOverview)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(BackupStage.EducationOverview, testSetup.getStage())
|
assertEquals(BackupStage.EducationOverview, testSetup.getStage())
|
||||||
|
@ -73,7 +73,7 @@ class BackupIntegrationTest : UiTestPrerequisites() {
|
||||||
val testSetup = newTestSetup(BackupStage.Test)
|
val testSetup = newTestSetup(BackupStage.Test)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(BackupStage.Test, testSetup.getStage())
|
assertEquals(BackupStage.Test, testSetup.getStage())
|
||||||
|
@ -102,7 +102,7 @@ class BackupIntegrationTest : UiTestPrerequisites() {
|
||||||
val testSetup = newTestSetup(BackupStage.Test)
|
val testSetup = newTestSetup(BackupStage.Test)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(0, testSetup.getOnChoicesCallbackCount())
|
assertEquals(0, testSetup.getOnChoicesCallbackCount())
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package co.electriccoin.zcash.ui.screen.backup.view
|
package co.electriccoin.zcash.ui.screen.backup.view
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
|
||||||
|
@ -50,9 +49,9 @@ class BackupTestSetup(
|
||||||
return state.current.value
|
return state.current.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ComposableNaming")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getDefaultContent() {
|
@Suppress("TestFunctionName")
|
||||||
|
fun DefaultContent() {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
BackupWallet(
|
BackupWallet(
|
||||||
PersistableWalletFixture.new(),
|
PersistableWalletFixture.new(),
|
||||||
|
@ -70,7 +69,7 @@ class BackupTestSetup(
|
||||||
|
|
||||||
fun setDefaultContent() {
|
fun setDefaultContent() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
getDefaultContent()
|
DefaultContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||||
import co.electriccoin.zcash.ui.screen.home.view.Home
|
import co.electriccoin.zcash.ui.screen.home.view.Home
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class HomeTestSetup(
|
class HomeTestSetup(
|
||||||
|
@ -54,10 +55,11 @@ class HomeTestSetup(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getDefaultContent() {
|
@Suppress("TestFunctionName")
|
||||||
|
fun DefaultContent() {
|
||||||
Home(
|
Home(
|
||||||
walletSnapshot,
|
walletSnapshot,
|
||||||
transactionHistory = emptyList(),
|
transactionHistory = persistentListOf(),
|
||||||
isKeepScreenOnDuringSync = false,
|
isKeepScreenOnDuringSync = false,
|
||||||
isUpdateAvailable = false,
|
isUpdateAvailable = false,
|
||||||
goSettings = {
|
goSettings = {
|
||||||
|
@ -86,7 +88,7 @@ class HomeTestSetup(
|
||||||
fun setDefaultContent() {
|
fun setDefaultContent() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
getDefaultContent()
|
DefaultContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ class HomeViewIntegrationTest : UiTestPrerequisites() {
|
||||||
val testSetup = newTestSetup(walletSnapshot)
|
val testSetup = newTestSetup(walletSnapshot)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
|
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package co.electriccoin.zcash.ui.screen.onboarding
|
package co.electriccoin.zcash.ui.screen.onboarding
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
@ -42,9 +41,9 @@ class OnboardingTestSetup(
|
||||||
return onboardingState.current.value
|
return onboardingState.current.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ComposableNaming")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getDefaultContent() {
|
@Suppress("TestFunctionName")
|
||||||
|
fun DefaultContent() {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
Onboarding(
|
Onboarding(
|
||||||
isFullOnboardingEnabled,
|
isFullOnboardingEnabled,
|
||||||
|
@ -60,7 +59,7 @@ class OnboardingTestSetup(
|
||||||
|
|
||||||
fun setDefaultContent() {
|
fun setDefaultContent() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
getDefaultContent()
|
DefaultContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ class OnboardingIntegrationTest : UiTestPrerequisites() {
|
||||||
val testSetup = newTestSetup(OnboardingStage.UnifiedAddresses)
|
val testSetup = newTestSetup(OnboardingStage.UnifiedAddresses)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(OnboardingStage.UnifiedAddresses, testSetup.getOnboardingStage())
|
assertEquals(OnboardingStage.UnifiedAddresses, testSetup.getOnboardingStage())
|
||||||
|
|
|
@ -10,6 +10,7 @@ import co.electriccoin.zcash.ui.common.LocalScreenSecurity
|
||||||
import co.electriccoin.zcash.ui.common.ScreenSecurity
|
import co.electriccoin.zcash.ui.common.ScreenSecurity
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||||
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -40,7 +41,7 @@ class RestoreViewSecuredScreenTest : UiTestPrerequisites() {
|
||||||
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
|
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
RestoreWallet(
|
RestoreWallet(
|
||||||
Mnemonics.getCachedWords(Locale.ENGLISH.language).toSortedSet(),
|
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
||||||
WordList(emptyList()),
|
WordList(emptyList()),
|
||||||
onBack = { },
|
onBack = { },
|
||||||
paste = { "" },
|
paste = { "" },
|
||||||
|
|
|
@ -26,6 +26,7 @@ import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||||
import co.electriccoin.zcash.ui.test.getAppContext
|
import co.electriccoin.zcash.ui.test.getAppContext
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -219,7 +220,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
RestoreWallet(
|
RestoreWallet(
|
||||||
Mnemonics.getCachedWords(Locale.ENGLISH.language).toSortedSet(),
|
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
||||||
state,
|
state,
|
||||||
onBack = {
|
onBack = {
|
||||||
onBackCount.incrementAndGet()
|
onBackCount.incrementAndGet()
|
||||||
|
|
|
@ -25,7 +25,8 @@ class ScanViewBasicTestSetup(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getDefaultContent() {
|
@Suppress("TestFunctionName")
|
||||||
|
fun DefaultContent() {
|
||||||
Scan(
|
Scan(
|
||||||
snackbarHostState = SnackbarHostState(),
|
snackbarHostState = SnackbarHostState(),
|
||||||
onBack = {
|
onBack = {
|
||||||
|
@ -42,7 +43,7 @@ class ScanViewBasicTestSetup(
|
||||||
fun setDefaultContent() {
|
fun setDefaultContent() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
getDefaultContent()
|
DefaultContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ class SupportViewIntegrationTest : UiTestPrerequisites() {
|
||||||
val testSetup = newTestSetup()
|
val testSetup = newTestSetup()
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("I can haz cheezburger?").also {
|
composeTestRule.onNodeWithText("I can haz cheezburger?").also {
|
||||||
|
@ -55,7 +55,7 @@ class SupportViewIntegrationTest : UiTestPrerequisites() {
|
||||||
val testSetup = newTestSetup()
|
val testSetup = newTestSetup()
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithText("I can haz cheezburger?").also {
|
composeTestRule.onNodeWithText("I can haz cheezburger?").also {
|
||||||
|
|
|
@ -31,7 +31,8 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getDefaultContent() {
|
@Suppress("TestFunctionName")
|
||||||
|
fun DefaultContent() {
|
||||||
Support(
|
Support(
|
||||||
SnackbarHostState(),
|
SnackbarHostState(),
|
||||||
onBack = {
|
onBack = {
|
||||||
|
@ -47,7 +48,7 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
|
||||||
fun setDefaultContent() {
|
fun setDefaultContent() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
getDefaultContent()
|
DefaultContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class UpdateViewIntegrationTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
restorationTester.setContent {
|
restorationTester.setContent {
|
||||||
testSetup.getDefaultContent()
|
testSetup.DefaultContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(testSetup.getUpdateInfo().priority, AppUpdateChecker.Priority.HIGH)
|
assertEquals(testSetup.getUpdateInfo().priority, AppUpdateChecker.Priority.HIGH)
|
||||||
|
|
|
@ -44,7 +44,8 @@ class UpdateViewTestSetup(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getDefaultContent() {
|
@Suppress("TestFunctionName")
|
||||||
|
fun DefaultContent() {
|
||||||
Update(
|
Update(
|
||||||
snackbarHostState = SnackbarHostState(),
|
snackbarHostState = SnackbarHostState(),
|
||||||
updateInfo = updateInfo,
|
updateInfo = updateInfo,
|
||||||
|
@ -64,7 +65,7 @@ class UpdateViewTestSetup(
|
||||||
fun setDefaultContent() {
|
fun setDefaultContent() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
getDefaultContent()
|
DefaultContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ class ScreenBrightness {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("CompositionLocalAllowlist")
|
||||||
val LocalScreenBrightness = compositionLocalOf { ScreenBrightness() }
|
val LocalScreenBrightness = compositionLocalOf { ScreenBrightness() }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -26,6 +26,7 @@ class ScreenSecurity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("CompositionLocalAllowlist")
|
||||||
val LocalScreenSecurity = compositionLocalOf { ScreenSecurity() }
|
val LocalScreenSecurity = compositionLocalOf { ScreenSecurity() }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -26,6 +26,7 @@ class ScreenTimeout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("CompositionLocalAllowlist")
|
||||||
val LocalScreenTimeout = compositionLocalOf { ScreenTimeout() }
|
val LocalScreenTimeout = compositionLocalOf { ScreenTimeout() }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -5,4 +5,5 @@ import co.electriccoin.zcash.configuration.model.map.Configuration
|
||||||
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
|
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
|
||||||
|
@Suppress("CompositionLocalAllowlist")
|
||||||
val RemoteConfig = compositionLocalOf<Configuration> { StringConfiguration(persistentMapOf(), null) }
|
val RemoteConfig = compositionLocalOf<Configuration> { StringConfiguration(persistentMapOf(), null) }
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
package co.electriccoin.zcash.ui.screen.backup.state
|
package co.electriccoin.zcash.ui.screen.backup.state
|
||||||
|
|
||||||
import co.electriccoin.zcash.spackle.model.Index
|
import co.electriccoin.zcash.spackle.model.Index
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
class TestChoices(initial: Map<Index, String?> = emptyMap()) {
|
class TestChoices(initial: Map<Index, String?> = emptyMap()) {
|
||||||
private val mutableState = MutableStateFlow<Map<Index, String?>>(HashMap(initial))
|
private val mutableState = MutableStateFlow<ImmutableMap<Index, String?>>(initial.toPersistentMap())
|
||||||
|
|
||||||
val current: StateFlow<Map<Index, String?>> = mutableState
|
val current: StateFlow<ImmutableMap<Index, String?>> = mutableState
|
||||||
|
|
||||||
fun set(map: Map<Index, String?>) {
|
fun set(map: Map<Index, String?>) {
|
||||||
mutableState.value = HashMap(map)
|
mutableState.value = map.toPersistentMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
|
|
@ -57,6 +57,8 @@ import co.electriccoin.zcash.ui.screen.backup.BackupTag
|
||||||
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
|
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
|
||||||
import co.electriccoin.zcash.ui.screen.backup.state.BackupState
|
import co.electriccoin.zcash.ui.screen.backup.state.BackupState
|
||||||
import co.electriccoin.zcash.ui.screen.backup.state.TestChoices
|
import co.electriccoin.zcash.ui.screen.backup.state.TestChoices
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
@Preview(device = Devices.PIXEL_4)
|
@Preview(device = Devices.PIXEL_4)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -145,7 +147,7 @@ fun BackupMainContent(
|
||||||
is BackupStage.Test -> TestInProgress(
|
is BackupStage.Test -> TestInProgress(
|
||||||
selectedTestChoices = choices,
|
selectedTestChoices = choices,
|
||||||
onChoicesChanged = onChoicesChanged,
|
onChoicesChanged = onChoicesChanged,
|
||||||
splitSeedPhrase = wallet.seedPhrase.split,
|
splitSeedPhrase = wallet.seedPhrase.split.toPersistentList(),
|
||||||
backupState = backupState
|
backupState = backupState
|
||||||
)
|
)
|
||||||
is BackupStage.Failure -> TestFailure()
|
is BackupStage.Failure -> TestFailure()
|
||||||
|
@ -202,7 +204,7 @@ private fun SeedPhrase(persistableWallet: PersistableWallet) {
|
||||||
.padding(vertical = ZcashTheme.paddings.padding)
|
.padding(vertical = ZcashTheme.paddings.padding)
|
||||||
) {
|
) {
|
||||||
Body(stringResource(R.string.new_wallet_3_body_1))
|
Body(stringResource(R.string.new_wallet_3_body_1))
|
||||||
ChipGrid(persistableWallet.seedPhrase.split)
|
ChipGrid(persistableWallet.seedPhrase.split.toPersistentList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +221,7 @@ private data class TestChoice(val originalIndex: Index, val word: String)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TestInProgress(
|
private fun TestInProgress(
|
||||||
splitSeedPhrase: List<String>,
|
splitSeedPhrase: ImmutableList<String>,
|
||||||
selectedTestChoices: TestChoices,
|
selectedTestChoices: TestChoices,
|
||||||
onChoicesChanged: ((choicesCount: Int) -> Unit)?,
|
onChoicesChanged: ((choicesCount: Int) -> Unit)?,
|
||||||
backupState: BackupState
|
backupState: BackupState
|
||||||
|
@ -258,20 +260,21 @@ private fun TestInProgress(
|
||||||
currentIndex,
|
currentIndex,
|
||||||
dropdownText = currentSelectedTestChoice[currentIndex]
|
dropdownText = currentSelectedTestChoice[currentIndex]
|
||||||
?: "",
|
?: "",
|
||||||
choices = testChoices.map { it.word },
|
choices = testChoices.map { it.word }.toPersistentList(),
|
||||||
|
{
|
||||||
|
selectedTestChoices.set(
|
||||||
|
HashMap(currentSelectedTestChoice).apply {
|
||||||
|
this[currentIndex] = testChoices[it.value].word
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (onChoicesChanged != null) {
|
||||||
|
onChoicesChanged(selectedTestChoices.current.value.size)
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(MINIMAL_WEIGHT)
|
.weight(MINIMAL_WEIGHT)
|
||||||
.testTag(BackupTag.DROPDOWN_CHIP)
|
.testTag(BackupTag.DROPDOWN_CHIP)
|
||||||
) {
|
)
|
||||||
selectedTestChoices.set(
|
|
||||||
HashMap(currentSelectedTestChoice).apply {
|
|
||||||
this[currentIndex] = testChoices[it.value].word
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (onChoicesChanged != null) {
|
|
||||||
onChoicesChanged(selectedTestChoices.current.value.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Chip(
|
Chip(
|
||||||
index = Index(chunkIndex * CHIP_GRID_ROW_SIZE + subIndex),
|
index = Index(chunkIndex * CHIP_GRID_ROW_SIZE + subIndex),
|
||||||
|
@ -388,25 +391,27 @@ private fun BackupTopAppBar(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CopySeedMenu(onCopyToClipboard: () -> Unit) {
|
private fun CopySeedMenu(onCopyToClipboard: () -> Unit) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
Column {
|
||||||
IconButton(onClick = { expanded = true }) {
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
Icon(
|
IconButton(onClick = { expanded = true }) {
|
||||||
Icons.Default.MoreVert,
|
Icon(
|
||||||
contentDescription = stringResource(R.string.new_wallet_toolbar_more_button_content_description)
|
Icons.Default.MoreVert,
|
||||||
)
|
contentDescription = stringResource(R.string.new_wallet_toolbar_more_button_content_description)
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false }
|
onDismissRequest = { expanded = false }
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.new_wallet_3_button_copy)) },
|
text = { Text(stringResource(R.string.new_wallet_3_button_copy)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
onCopyToClipboard()
|
onCopyToClipboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import co.electriccoin.zcash.spackle.model.Index
|
||||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.backup.BackupTag
|
import co.electriccoin.zcash.ui.screen.backup.BackupTag
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param chipIndex The index of the chip, which is displayed to the user.
|
* @param chipIndex The index of the chip, which is displayed to the user.
|
||||||
|
@ -36,17 +37,15 @@ import co.electriccoin.zcash.ui.screen.backup.BackupTag
|
||||||
* @param choices Item choices to display in the open drop down menu. Positional index is important.
|
* @param choices Item choices to display in the open drop down menu. Positional index is important.
|
||||||
* @param onChoiceSelected Callback with the positional index of the item the user selected from [choices].
|
* @param onChoiceSelected Callback with the positional index of the item the user selected from [choices].
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChipDropDown(
|
fun ChipDropDown(
|
||||||
chipIndex: Index,
|
chipIndex: Index,
|
||||||
dropdownText: String,
|
dropdownText: String,
|
||||||
choices: List<String>,
|
choices: ImmutableList<String>,
|
||||||
modifier: Modifier = Modifier,
|
onChoiceSelected: (Index) -> Unit,
|
||||||
onChoiceSelected: (Index) -> Unit
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.then(
|
modifier = modifier.then(
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -71,7 +70,7 @@ fun ChipDropDown(
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSecondary
|
color = MaterialTheme.colorScheme.onSecondary
|
||||||
)
|
)
|
||||||
Spacer(modifier = modifier.fillMaxWidth(MINIMAL_WEIGHT))
|
Spacer(modifier = Modifier.fillMaxWidth(MINIMAL_WEIGHT))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowDropDown,
|
imageVector = Icons.Filled.ArrowDropDown,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
|
@ -73,6 +73,8 @@ import co.electriccoin.zcash.ui.screen.home.HomeTag
|
||||||
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
|
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
|
||||||
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues
|
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues
|
||||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
@ -82,7 +84,7 @@ fun ComposablePreview() {
|
||||||
GradientSurface {
|
GradientSurface {
|
||||||
Home(
|
Home(
|
||||||
walletSnapshot = WalletSnapshotFixture.new(),
|
walletSnapshot = WalletSnapshotFixture.new(),
|
||||||
transactionHistory = emptyList(),
|
transactionHistory = persistentListOf(),
|
||||||
isUpdateAvailable = false,
|
isUpdateAvailable = false,
|
||||||
isKeepScreenOnDuringSync = false,
|
isKeepScreenOnDuringSync = false,
|
||||||
isDebugMenuEnabled = false,
|
isDebugMenuEnabled = false,
|
||||||
|
@ -103,7 +105,7 @@ fun ComposablePreview() {
|
||||||
@Composable
|
@Composable
|
||||||
fun Home(
|
fun Home(
|
||||||
walletSnapshot: WalletSnapshot,
|
walletSnapshot: WalletSnapshot,
|
||||||
transactionHistory: List<CommonTransaction>,
|
transactionHistory: ImmutableList<CommonTransaction>,
|
||||||
isUpdateAvailable: Boolean,
|
isUpdateAvailable: Boolean,
|
||||||
isKeepScreenOnDuringSync: Boolean?,
|
isKeepScreenOnDuringSync: Boolean?,
|
||||||
isDebugMenuEnabled: Boolean,
|
isDebugMenuEnabled: Boolean,
|
||||||
|
@ -181,39 +183,41 @@ private fun HomeTopAppBar(
|
||||||
private fun DebugMenu(
|
private fun DebugMenu(
|
||||||
resetSdk: () -> Unit
|
resetSdk: () -> Unit
|
||||||
) {
|
) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
Column {
|
||||||
IconButton(onClick = { expanded = true }) {
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
IconButton(onClick = { expanded = true }) {
|
||||||
}
|
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false }
|
onDismissRequest = { expanded = false }
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Throw Uncaught Exception") },
|
text = { Text("Throw Uncaught Exception") },
|
||||||
onClick = {
|
onClick = {
|
||||||
// Supposed to be generic, for manual debugging only
|
// Supposed to be generic, for manual debugging only
|
||||||
@Suppress("TooGenericExceptionThrown")
|
@Suppress("TooGenericExceptionThrown")
|
||||||
throw RuntimeException("Manually crashed from debug menu")
|
throw RuntimeException("Manually crashed from debug menu")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Report Caught Exception") },
|
text = { Text("Report Caught Exception") },
|
||||||
onClick = {
|
onClick = {
|
||||||
// Eventually this shouldn't rely on the Android implementation, but rather an expect/actual
|
// Eventually this shouldn't rely on the Android implementation, but rather an expect/actual
|
||||||
// should be used at the crash API level.
|
// should be used at the crash API level.
|
||||||
GlobalCrashReporter.reportCaughtException(RuntimeException("Manually caught exception from debug menu"))
|
GlobalCrashReporter.reportCaughtException(RuntimeException("Manually caught exception from debug menu"))
|
||||||
expanded = false
|
expanded = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Reset SDK") },
|
text = { Text("Reset SDK") },
|
||||||
onClick = {
|
onClick = {
|
||||||
resetSdk()
|
resetSdk()
|
||||||
expanded = false
|
expanded = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,7 +279,7 @@ private fun HomeDrawer(
|
||||||
private fun HomeMainContent(
|
private fun HomeMainContent(
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
walletSnapshot: WalletSnapshot,
|
walletSnapshot: WalletSnapshot,
|
||||||
transactionHistory: List<CommonTransaction>,
|
transactionHistory: ImmutableList<CommonTransaction>,
|
||||||
isUpdateAvailable: Boolean,
|
isUpdateAvailable: Boolean,
|
||||||
isKeepScreenOnDuringSync: Boolean?,
|
isKeepScreenOnDuringSync: Boolean?,
|
||||||
goReceive: () -> Unit,
|
goReceive: () -> Unit,
|
||||||
|
@ -406,7 +410,7 @@ private fun Status(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
private fun History(transactionHistory: List<CommonTransaction>) {
|
private fun History(transactionHistory: ImmutableList<CommonTransaction>) {
|
||||||
if (transactionHistory.isEmpty()) {
|
if (transactionHistory.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,9 @@ import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
|
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
|
||||||
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
|
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
|
||||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
@ -151,10 +154,10 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
|
|
||||||
// This is not the right API, because the transaction list could be very long and might need UI filtering
|
// This is not the right API, because the transaction list could be very long and might need UI filtering
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val transactionSnapshot: StateFlow<List<CommonTransaction>> = synchronizer
|
val transactionSnapshot: StateFlow<ImmutableList<CommonTransaction>> = synchronizer
|
||||||
.flatMapLatest {
|
.flatMapLatest {
|
||||||
if (null == it) {
|
if (null == it) {
|
||||||
flowOf(emptyList())
|
flowOf(persistentListOf())
|
||||||
} else {
|
} else {
|
||||||
it.toTransactions()
|
it.toTransactions()
|
||||||
}
|
}
|
||||||
|
@ -162,7 +165,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||||
emptyList()
|
persistentListOf()
|
||||||
)
|
)
|
||||||
|
|
||||||
val addresses: StateFlow<WalletAddresses?> = synchronizer
|
val addresses: StateFlow<WalletAddresses?> = synchronizer
|
||||||
|
@ -360,7 +363,7 @@ private fun Synchronizer.toWalletSnapshot() =
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Synchronizer.toTransactions() =
|
private fun Synchronizer.toTransactions(): Flow<ImmutableList<CommonTransaction>> =
|
||||||
combine(
|
combine(
|
||||||
clearedTransactions.distinctUntilChanged(),
|
clearedTransactions.distinctUntilChanged(),
|
||||||
pendingTransactions.distinctUntilChanged()
|
pendingTransactions.distinctUntilChanged()
|
||||||
|
@ -372,5 +375,5 @@ private fun Synchronizer.toTransactions() =
|
||||||
buildList {
|
buildList {
|
||||||
addAll(cleared.map { CommonTransaction.Overview(it) })
|
addAll(cleared.map { CommonTransaction.Overview(it) })
|
||||||
addAll(pending.map { CommonTransaction.Pending(it) })
|
addAll(pending.map { CommonTransaction.Pending(it) })
|
||||||
}
|
}.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,19 +137,21 @@ private fun OnboardingTopAppBar(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DebugMenu(onFixtureWallet: () -> Unit) {
|
private fun DebugMenu(onFixtureWallet: () -> Unit) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
Column {
|
||||||
IconButton(onClick = { expanded = true }) {
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
IconButton(onClick = { expanded = true }) {
|
||||||
}
|
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false }
|
onDismissRequest = { expanded = false }
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Import wallet with fixture seed phrase") },
|
text = { Text("Import wallet with fixture seed phrase") },
|
||||||
onClick = onFixtureWallet
|
onClick = onFixtureWallet
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,23 +109,24 @@ private fun ReceiveContents(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun QrCode(data: String, size: Dp, modifier: Modifier) {
|
private fun QrCode(data: String, size: Dp, modifier: Modifier = Modifier) {
|
||||||
BrightenScreen()
|
Column(modifier = modifier) {
|
||||||
DisableScreenTimeout()
|
BrightenScreen()
|
||||||
val sizePixels = with(LocalDensity.current) { size.toPx() }.roundToInt()
|
DisableScreenTimeout()
|
||||||
|
val sizePixels = with(LocalDensity.current) { size.toPx() }.roundToInt()
|
||||||
|
|
||||||
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
|
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
|
||||||
|
|
||||||
// Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform
|
// Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform
|
||||||
// representation. This should have minimal performance impact since the QR code is relatively
|
// representation. This should have minimal performance impact since the QR code is relatively
|
||||||
// small and we only generate QR codes infrequently.
|
// small and we only generate QR codes infrequently.
|
||||||
|
|
||||||
val qrCodePixelArray = JvmQrCodeGenerator.generate(data, sizePixels)
|
val qrCodePixelArray = JvmQrCodeGenerator.generate(data, sizePixels)
|
||||||
val qrCodeImage = AndroidQrCodeImageGenerator.generate(qrCodePixelArray, sizePixels)
|
val qrCodeImage = AndroidQrCodeImageGenerator.generate(qrCodePixelArray, sizePixels)
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
bitmap = qrCodeImage,
|
bitmap = qrCodeImage,
|
||||||
contentDescription = stringResource(R.string.receive_qr_code_content_description),
|
contentDescription = stringResource(R.string.receive_qr_code_content_description)
|
||||||
modifier = modifier
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
package co.electriccoin.zcash.ui.screen.restore.state
|
package co.electriccoin.zcash.ui.screen.restore.state
|
||||||
|
|
||||||
import cash.z.ecc.sdk.model.SeedPhraseValidation
|
import cash.z.ecc.sdk.model.SeedPhraseValidation
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class WordList(initial: List<String> = emptyList()) {
|
class WordList(initial: List<String> = emptyList()) {
|
||||||
private val mutableState = MutableStateFlow(initial)
|
private val mutableState: MutableStateFlow<ImmutableList<String>> = MutableStateFlow(initial.toPersistentList())
|
||||||
|
|
||||||
val current: StateFlow<List<String>> = mutableState
|
val current: StateFlow<ImmutableList<String>> = mutableState
|
||||||
|
|
||||||
fun set(list: List<String>) {
|
fun set(list: List<String>) {
|
||||||
mutableState.value = ArrayList(list)
|
mutableState.value = list.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun append(words: List<String>) {
|
fun append(words: List<String>) {
|
||||||
mutableState.value = ArrayList(current.value) + words
|
mutableState.value = (current.value + words).toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom toString to prevent leaking word list
|
// Custom toString to prevent leaking word list
|
||||||
|
|
|
@ -70,6 +70,9 @@ import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
||||||
import co.electriccoin.zcash.ui.screen.restore.model.ParseResult
|
import co.electriccoin.zcash.ui.screen.restore.model.ParseResult
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.wordValidation
|
import co.electriccoin.zcash.ui.screen.restore.state.wordValidation
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
import kotlinx.collections.immutable.persistentHashSetOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Preview("Restore Wallet")
|
@Preview("Restore Wallet")
|
||||||
|
@ -78,7 +81,7 @@ fun PreviewRestore() {
|
||||||
ZcashTheme(darkTheme = true) {
|
ZcashTheme(darkTheme = true) {
|
||||||
GradientSurface {
|
GradientSurface {
|
||||||
RestoreWallet(
|
RestoreWallet(
|
||||||
completeWordList = setOf(
|
completeWordList = persistentHashSetOf(
|
||||||
"abandon",
|
"abandon",
|
||||||
"ability",
|
"ability",
|
||||||
"able",
|
"able",
|
||||||
|
@ -113,7 +116,7 @@ fun PreviewRestoreComplete() {
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RestoreWallet(
|
fun RestoreWallet(
|
||||||
completeWordList: Set<String>,
|
completeWordList: ImmutableSet<String>,
|
||||||
userWordList: WordList,
|
userWordList: WordList,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
paste: () -> String?,
|
paste: () -> String?,
|
||||||
|
@ -131,16 +134,16 @@ fun RestoreWallet(
|
||||||
}, bottomBar = {
|
}, bottomBar = {
|
||||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||||
Warn(parseResult)
|
Warn(parseResult)
|
||||||
Autocomplete(parseResult = parseResult) {
|
Autocomplete(parseResult = parseResult, {
|
||||||
textState = ""
|
textState = ""
|
||||||
userWordList.append(listOf(it))
|
userWordList.append(listOf(it))
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
})
|
||||||
NextWordTextField(
|
NextWordTextField(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
parseResult = parseResult,
|
parseResult = parseResult,
|
||||||
text = textState,
|
text = textState,
|
||||||
setText = { textState = it }
|
setText = { textState = it },
|
||||||
|
modifier = Modifier.focusRequester(focusRequester)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}) { paddingValues ->
|
}) { paddingValues ->
|
||||||
|
@ -236,7 +239,7 @@ private fun RestoreMainContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChipGridWithText(
|
private fun ChipGridWithText(
|
||||||
userWordList: List<String>
|
userWordList: ImmutableList<String>
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -268,10 +271,10 @@ private fun ChipGridWithText(
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
private fun NextWordTextField(
|
private fun NextWordTextField(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
parseResult: ParseResult,
|
parseResult: ParseResult,
|
||||||
text: String,
|
text: String,
|
||||||
setText: (String) -> Unit
|
setText: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
* Treat the user input as a password, but disable the transformation to obscure input.
|
* Treat the user input as a password, but disable the transformation to obscure input.
|
||||||
|
@ -285,7 +288,7 @@ private fun NextWordTextField(
|
||||||
shadowElevation = 8.dp
|
shadowElevation = 8.dp
|
||||||
) {
|
) {
|
||||||
TextField(
|
TextField(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.testTag(RestoreTag.SEED_WORD_TEXT_FIELD),
|
.testTag(RestoreTag.SEED_WORD_TEXT_FIELD),
|
||||||
|
@ -312,9 +315,9 @@ private fun NextWordTextField(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Autocomplete(
|
private fun Autocomplete(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
parseResult: ParseResult,
|
parseResult: ParseResult,
|
||||||
onSuggestionSelected: (String) -> Unit
|
onSuggestionSelected: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val (isHighlight, suggestions) = when (parseResult) {
|
val (isHighlight, suggestions) = when (parseResult) {
|
||||||
is ParseResult.Autocomplete -> {
|
is ParseResult.Autocomplete -> {
|
||||||
|
@ -336,6 +339,7 @@ private fun Autocomplete(
|
||||||
modifier
|
modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ModifierReused")
|
||||||
LazyRow(highlightModifier.testTag(RestoreTag.AUTOCOMPLETE_LAYOUT)) {
|
LazyRow(highlightModifier.testTag(RestoreTag.AUTOCOMPLETE_LAYOUT)) {
|
||||||
items(it) {
|
items(it) {
|
||||||
Chip(
|
Chip(
|
||||||
|
|
|
@ -8,6 +8,8 @@ import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.sdk.ext.collectWith
|
import cash.z.ecc.android.sdk.ext.collectWith
|
||||||
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
|
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||||
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.WhileSubscribed
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
|
@ -15,7 +17,6 @@ import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TreeSet
|
|
||||||
|
|
||||||
class RestoreViewModel(application: Application, savedStateHandle: SavedStateHandle) : AndroidViewModel(application) {
|
class RestoreViewModel(application: Application, savedStateHandle: SavedStateHandle) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ class RestoreViewModel(application: Application, savedStateHandle: SavedStateHan
|
||||||
Mnemonics.getCachedWords(Locale.ENGLISH.language)
|
Mnemonics.getCachedWords(Locale.ENGLISH.language)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(CompleteWordSetState.Loaded(TreeSet(completeWordList)))
|
emit(CompleteWordSetState.Loaded(completeWordList.toPersistentSet()))
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||||
|
@ -54,7 +55,7 @@ class RestoreViewModel(application: Application, savedStateHandle: SavedStateHan
|
||||||
// viewModelScope is constructed with Dispatchers.Main.immediate, so this will
|
// viewModelScope is constructed with Dispatchers.Main.immediate, so this will
|
||||||
// update the save state as soon as a change occurs.
|
// update the save state as soon as a change occurs.
|
||||||
userWordList.current.collectWith(viewModelScope) {
|
userWordList.current.collectWith(viewModelScope) {
|
||||||
savedStateHandle.set(KEY_WORD_LIST, it)
|
savedStateHandle[KEY_WORD_LIST] = ArrayList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,5 +66,5 @@ class RestoreViewModel(application: Application, savedStateHandle: SavedStateHan
|
||||||
|
|
||||||
sealed class CompleteWordSetState {
|
sealed class CompleteWordSetState {
|
||||||
object Loading : CompleteWordSetState()
|
object Loading : CompleteWordSetState()
|
||||||
data class Loaded(val list: Set<String>) : CompleteWordSetState()
|
data class Loaded(val list: ImmutableSet<String>) : CompleteWordSetState()
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import co.electriccoin.zcash.ui.design.component.ChipGrid
|
||||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||||
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
@Preview("Seed")
|
@Preview("Seed")
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -95,7 +96,7 @@ private fun SeedMainContent(
|
||||||
) {
|
) {
|
||||||
Body(stringResource(R.string.seed_body))
|
Body(stringResource(R.string.seed_body))
|
||||||
|
|
||||||
ChipGrid(persistableWallet.seedPhrase.split)
|
ChipGrid(persistableWallet.seedPhrase.split.toPersistentList())
|
||||||
|
|
||||||
TertiaryButton(onClick = onCopyToClipboard, text = stringResource(R.string.seed_copy))
|
TertiaryButton(onClick = onCopyToClipboard, text = stringResource(R.string.seed_copy))
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,9 +72,9 @@ fun PreviewSend() {
|
||||||
@Composable
|
@Composable
|
||||||
fun Send(
|
fun Send(
|
||||||
mySpendableBalance: Zatoshi,
|
mySpendableBalance: Zatoshi,
|
||||||
pressAndHoldInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
onCreateAndSend: (ZecSend) -> Unit
|
onCreateAndSend: (ZecSend) -> Unit,
|
||||||
|
pressAndHoldInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
// For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might
|
// For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might
|
||||||
// change once deep-linking support is added. It depends on whether deep linking should do one of:
|
// change once deep-linking support is added. It depends on whether deep linking should do one of:
|
||||||
|
@ -272,9 +272,10 @@ private fun Confirmation(
|
||||||
|
|
||||||
TimedButton(
|
TimedButton(
|
||||||
onClick = onConfirmation,
|
onClick = onConfirmation,
|
||||||
|
{
|
||||||
|
Text(text = stringResource(id = R.string.send_confirm))
|
||||||
|
},
|
||||||
interactionSource = pressAndHoldInteractionSource
|
interactionSource = pressAndHoldInteractionSource
|
||||||
) {
|
)
|
||||||
Text(text = stringResource(id = R.string.send_confirm))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,22 +122,24 @@ private fun SettingsTopAppBar(
|
||||||
private fun TroubleshootingMenu(
|
private fun TroubleshootingMenu(
|
||||||
onRescanWallet: () -> Unit
|
onRescanWallet: () -> Unit
|
||||||
) {
|
) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
Column {
|
||||||
IconButton(onClick = { expanded = true }) {
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.settings_overflow_content_description))
|
IconButton(onClick = { expanded = true }) {
|
||||||
}
|
Icon(Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.settings_overflow_content_description))
|
||||||
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false }
|
onDismissRequest = { expanded = false }
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(id = R.string.settings_rescan)) },
|
text = { Text(stringResource(id = R.string.settings_rescan)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
onRescanWallet()
|
onRescanWallet()
|
||||||
expanded = false
|
expanded = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,9 +48,9 @@ fun NotEnoughSpaceView(storageSpaceRequiredGigabytes: Int, spaceRequiredToContin
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(64.dp))
|
Spacer(Modifier.height(64.dp))
|
||||||
Small(
|
Small(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = stringResource(id = R.string.space_required_to_continue, spaceRequiredToContinueMegabytes),
|
text = stringResource(id = R.string.space_required_to_continue, spaceRequiredToContinueMegabytes),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue