From d97e87b89c98abfb658c28d364ee8074eb28c4c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 11:44:52 +0000 Subject: [PATCH 1/6] Remove mutable state from AmberBunkerRequest and IntentData Replaces MutableState (checked) and MutableState (rememberType) fields in AmberBunkerRequest and IntentData with plain immutable values. UI state is now managed in MultiEventScreenIntents via a SnapshotStateMap (checkedStates) and a mutableStateOf (rememberType), keeping Compose state out of pure data models and fixing data class semantics (copy/equals/hashCode). https://claude.ai/code/session_01PRSKjmFms1Tj1QJ5FhfqZm --- .../nostrsigner/models/AmberBunkerRequest.kt | 5 -- .../nostrsigner/models/IntentData.kt | 4 - .../nostrsigner/service/IntentUtils.kt | 17 ---- .../service/MultiEventScreenIntents.kt | 7 ++ .../nostrsigner/ui/SeeDetailsScreen.kt | 24 +++--- .../components/BunkerMultiEventHomeScreen.kt | 79 ++++++++++++------- .../ui/components/BunkerPermissionCard.kt | 7 +- .../components/IntentMultiEventHomeScreen.kt | 75 +++++++++++------- .../ui/components/PermissionCard.kt | 7 +- 9 files changed, 125 insertions(+), 100 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt index 3e198b64..8d640df9 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt @@ -1,8 +1,5 @@ package com.greenart7c3.nostrsigner.models -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import com.greenart7c3.nostrsigner.ui.RememberType import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest @@ -22,8 +19,6 @@ data class AmberBunkerRequest( val name: String, val signedEvent: Event?, val encryptedData: EncryptedDataKind?, - val checked: MutableState = mutableStateOf(true), - val rememberType: MutableState = mutableStateOf(RememberType.NEVER), val encryptionType: EncryptionType, val isNostrConnectUri: Boolean, ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt index f953bf62..fbc463e8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt @@ -1,7 +1,5 @@ package com.greenart7c3.nostrsigner.models -import androidx.compose.runtime.MutableState -import com.greenart7c3.nostrsigner.ui.RememberType import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -16,8 +14,6 @@ data class IntentData( val returnType: ReturnType, val permissions: List?, val currentAccount: String, - val checked: MutableState, - val rememberType: MutableState, val route: String?, val event: Event?, val encryptedData: EncryptedDataKind?, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt index b4010e7e..1043b07f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -8,7 +8,6 @@ import android.net.Uri import android.provider.Browser import android.util.Log import android.widget.Toast -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.Clipboard import androidx.compose.ui.text.intl.Locale @@ -173,8 +172,6 @@ object IntentUtils { returnType = returnType, permissions = listOf(), currentAccount = Hex.decode(signedEvent.pubKey).toNpub(), - checked = mutableStateOf(true), - rememberType = mutableStateOf(RememberType.NEVER), route = route, event = signedEvent, encryptedData = null, @@ -219,8 +216,6 @@ object IntentUtils { returnType = returnType, permissions = listOf(), currentAccount = Hex.decode(pubKey).toNpub(), - checked = mutableStateOf(true), - rememberType = mutableStateOf(RememberType.NEVER), route = route, event = null, encryptedData = encryptedDataKind, @@ -238,8 +233,6 @@ object IntentUtils { returnType = returnType, permissions = listOf(), currentAccount = "", - checked = mutableStateOf(true), - rememberType = mutableStateOf(RememberType.NEVER), route = route, event = null, encryptedData = null, @@ -257,8 +250,6 @@ object IntentUtils { returnType = returnType, permissions = listOf(), currentAccount = "", - checked = mutableStateOf(true), - rememberType = mutableStateOf(RememberType.NEVER), route = route, event = null, encryptedData = null, @@ -359,8 +350,6 @@ object IntentUtils { returnType = returnType, permissions = permissions?.map { Permission(it.type.trim(), it.kind, it.checked) }, currentAccount = npub ?: Hex.decode(signed.pubKey).toNpub(), - checked = mutableStateOf(true), - rememberType = mutableStateOf(RememberType.NEVER), route = route, event = signed, encryptedData = null, @@ -411,8 +400,6 @@ object IntentUtils { returnType = returnType, permissions = permissions?.map { Permission(it.type.trim(), it.kind, it.checked) }, currentAccount = npub ?: Hex.decode(pubKey).toNpub(), - checked = mutableStateOf(true), - rememberType = mutableStateOf(RememberType.NEVER), route = route, event = null, encryptedData = encryptedDataKind, @@ -435,8 +422,6 @@ object IntentUtils { returnType = returnType, permissions = permissions?.map { Permission(it.type.trim(), it.kind, it.checked) }, currentAccount = npub ?: Hex.decode(pubKey).toNpub(), - checked = mutableStateOf(true), - rememberType = mutableStateOf(RememberType.NEVER), route = route, event = null, encryptedData = null, @@ -459,8 +444,6 @@ object IntentUtils { returnType = returnType, permissions = permissions?.map { Permission(it.type.trim(), it.kind, it.checked) }, currentAccount = npub ?: Hex.decode(pubKey).toNpub(), - checked = mutableStateOf(true), - rememberType = mutableStateOf(RememberType.NEVER), route = route, event = null, encryptedData = null, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/MultiEventScreenIntents.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/MultiEventScreenIntents.kt index 7d394c0b..865340d0 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/MultiEventScreenIntents.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/MultiEventScreenIntents.kt @@ -1,10 +1,17 @@ package com.greenart7c3.nostrsigner.service +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.greenart7c3.nostrsigner.models.AmberBunkerRequest import com.greenart7c3.nostrsigner.models.IntentData +import com.greenart7c3.nostrsigner.ui.RememberType object MultiEventScreenIntents { var intents = listOf() var bunkerRequests = listOf() var appName = "" + val checkedStates = mutableStateMapOf() + var rememberType by mutableStateOf(RememberType.NEVER) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SeeDetailsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SeeDetailsScreen.kt index 7ad7f4b8..f71b044c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SeeDetailsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SeeDetailsScreen.kt @@ -56,7 +56,7 @@ fun SeeDetailsScreen( .weight(1f) .verticalScroll(rememberScrollState()), ) { - var rememberType by remember { mutableStateOf(MultiEventScreenIntents.intents.firstOrNull()?.rememberType?.value ?: MultiEventScreenIntents.bunkerRequests.first().rememberType.value) } + var rememberType by remember { mutableStateOf(MultiEventScreenIntents.rememberType) } val type = if (MultiEventScreenIntents.intents.isNotEmpty()) { MultiEventScreenIntents.intents.first().type } else { @@ -96,15 +96,14 @@ fun SeeDetailsScreen( onReject = {}, onChanged = { rememberType = it - MultiEventScreenIntents.intents.forEach { intent -> - intent.rememberType.value = rememberType - } + MultiEventScreenIntents.rememberType = it }, packageName = null, ) } MultiEventScreenIntents.intents.forEach { intent -> + val intentChecked = MultiEventScreenIntents.checkedStates[intent.id] ?: true Card( Modifier .padding(4.dp), @@ -118,13 +117,13 @@ fun SeeDetailsScreen( modifier = Modifier .fillMaxWidth() .clickable { - intent.checked.value = !intent.checked.value + MultiEventScreenIntents.checkedStates[intent.id] = !intentChecked }, ) { Checkbox( - checked = intent.checked.value, + checked = intentChecked, onCheckedChange = { _ -> - intent.checked.value = !intent.checked.value + MultiEventScreenIntents.checkedStates[intent.id] = !intentChecked }, colors = CheckboxDefaults.colors().copy( uncheckedBorderColor = Color.Gray, @@ -163,13 +162,14 @@ fun SeeDetailsScreen( .weight(1f) .padding(vertical = 8.dp), text = data.ifBlank { message }, - color = if (intent.checked.value) Color.Unspecified else Color.Gray, + color = if (intentChecked) Color.Unspecified else Color.Gray, ) } } } MultiEventScreenIntents.bunkerRequests.forEach { bunkerRequest -> + val bunkerChecked = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true Card( Modifier .padding(4.dp), @@ -183,13 +183,13 @@ fun SeeDetailsScreen( modifier = Modifier .fillMaxWidth() .clickable { - bunkerRequest.checked.value = !bunkerRequest.checked.value + MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] = !bunkerChecked }, ) { Checkbox( - checked = bunkerRequest.checked.value, + checked = bunkerChecked, onCheckedChange = { _ -> - bunkerRequest.checked.value = !bunkerRequest.checked.value + MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] = !bunkerChecked }, colors = CheckboxDefaults.colors().copy( uncheckedBorderColor = Color.Gray, @@ -228,7 +228,7 @@ fun SeeDetailsScreen( .weight(1f) .padding(vertical = 8.dp), text = data.ifBlank { message }, - color = if (bunkerRequest.checked.value) Color.Unspecified else Color.Gray, + color = if (bunkerChecked) Color.Unspecified else Color.Gray, ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index f7cf3594..716dd559 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -53,6 +53,7 @@ import com.greenart7c3.nostrsigner.service.AmberUtils import com.greenart7c3.nostrsigner.service.ApplicationNameCache import com.greenart7c3.nostrsigner.service.BunkerRequestUtils import com.greenart7c3.nostrsigner.service.EventNotificationConsumer +import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType @@ -82,6 +83,12 @@ fun BunkerMultiEventHomeScreen( var relayAuthScope by remember { mutableStateOf(RelayAuthScope.SPECIFIC) } var appName by remember { mutableStateOf(ApplicationNameCache.names["$localAccount-$key"] ?: key.toShortenHex()) } + LaunchedEffect(Unit) { + MultiEventScreenIntents.checkedStates.clear() + MultiEventScreenIntents.rememberType = RememberType.NEVER + bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = true } + } + LaunchedEffect(Unit) { launch(Dispatchers.IO) { localAccount = LocalPreferences.loadFromEncryptedStorage( @@ -116,8 +123,8 @@ fun BunkerMultiEventHomeScreen( SigningAs(accountParam) val allCheckedState = when { - bunkerRequests.all { it.checked.value } -> ToggleableState.On - bunkerRequests.none { it.checked.value } -> ToggleableState.Off + bunkerRequests.all { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.On + bunkerRequests.none { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.Off else -> ToggleableState.Indeterminate } Row( @@ -126,14 +133,14 @@ fun BunkerMultiEventHomeScreen( .fillMaxWidth() .clickable { val newValue = allCheckedState != ToggleableState.On - bunkerRequests.forEach { it.checked.value = newValue } + bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = newValue } }, ) { TriStateCheckbox( state = allCheckedState, onClick = { val newValue = allCheckedState != ToggleableState.On - bunkerRequests.forEach { it.checked.value = newValue } + bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = newValue } }, ) Text(stringResource(R.string.select_deselect_all)) @@ -145,7 +152,15 @@ fun BunkerMultiEventHomeScreen( .verticalScroll(rememberScrollState()), ) { bunkerRequests.forEach { bunkerRequest -> - BunkerRequestCard(context = context, bunkerRequest = bunkerRequest) + BunkerRequestCard( + context = context, + bunkerRequest = bunkerRequest, + checked = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true, + onToggleChecked = { + val current = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true + MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] = !current + }, + ) } } @@ -184,9 +199,7 @@ fun BunkerMultiEventHomeScreen( onReject = {}, onChanged = { rememberType = it - bunkerRequests.forEach { bunkerRequest -> - bunkerRequest.rememberType.value = rememberType - } + MultiEventScreenIntents.rememberType = it }, packageName = packageName, ) @@ -247,7 +260,8 @@ fun BunkerMultiEventHomeScreen( permissions = mutableListOf(), ) - if (request.rememberType.value != RememberType.NEVER && request.checked.value) { + val isChecked = MultiEventScreenIntents.checkedStates[request.request.id] ?: true + if (rememberType != RememberType.NEVER && isChecked) { val rejectKind = if (request.request is BunkerRequestSign) request.request.event.kind else null val rejectRelay = if (request.request is BunkerRequestSign && request.request.event.kind == 22242) { if (relayAuthScope == RelayAuthScope.ALL) { @@ -272,7 +286,7 @@ fun BunkerMultiEventHomeScreen( BunkerRequestUtils.getTypeFromBunker(request.request), rejectKind, false, - request.rememberType.value, + rememberType, thisAccount, relay = rejectRelay, ) @@ -345,10 +359,12 @@ fun BunkerMultiEventHomeScreen( permissions = mutableListOf(), ) + val isChecked = MultiEventScreenIntents.checkedStates[request.request.id] ?: true + if (request.request is BunkerRequestSign) { val localEvent = request.signedEvent!! - if (request.rememberType.value != RememberType.NEVER && request.checked.value) { + if (rememberType != RememberType.NEVER && isChecked) { val signRelay = if (localEvent.kind == 22242) { if (relayAuthScope == RelayAuthScope.ALL) { "*" @@ -372,7 +388,7 @@ fun BunkerMultiEventHomeScreen( signerType = SignerType.SIGN_EVENT, kind = localEvent.kind, value = true, - rememberType = request.rememberType.value, + rememberType = rememberType, account = thisAccount, relay = signRelay, ) @@ -387,7 +403,7 @@ fun BunkerMultiEventHomeScreen( type = SignerType.SIGN_EVENT.toString(), kind = localEvent.kind, time = TimeUtils.now(), - accepted = request.checked.value, + accepted = isChecked, content = localEvent.toJson(), ), thisAccount.npub, @@ -395,7 +411,7 @@ fun BunkerMultiEventHomeScreen( BunkerRequestUtils.remove(request.request.id) - if (request.checked.value) { + if (isChecked) { BunkerRequestUtils.sendBunkerResponse( context, thisAccount, @@ -416,14 +432,14 @@ fun BunkerMultiEventHomeScreen( ) } } else if (request.request.method == "sign_message") { - if (request.rememberType.value != RememberType.NEVER && request.checked.value) { + if (rememberType != RememberType.NEVER && isChecked) { AmberUtils.acceptOrRejectPermission( application, localKey, SignerType.SIGN_MESSAGE, null, true, - request.rememberType.value, + rememberType, thisAccount, ) } @@ -436,7 +452,7 @@ fun BunkerMultiEventHomeScreen( SignerType.SIGN_MESSAGE.toString(), null, TimeUtils.now(), - request.checked.value, + isChecked, content = request.request.params.first(), ), thisAccount.npub, @@ -445,7 +461,7 @@ fun BunkerMultiEventHomeScreen( val signedMessage = thisAccount.signString(request.request.params.first()) BunkerRequestUtils.remove(request.request.id) - if (request.checked.value) { + if (isChecked) { BunkerRequestUtils.sendBunkerResponse( context, thisAccount, @@ -476,14 +492,14 @@ fun BunkerMultiEventHomeScreen( SignerType.CONNECT.toString(), null, TimeUtils.now(), - request.checked.value, + isChecked, content = "", ), thisAccount.npub, ) BunkerRequestUtils.remove(request.request.id) - if (request.checked.value) { + if (isChecked) { BunkerRequestUtils.sendBunkerResponse( context, thisAccount, @@ -506,14 +522,14 @@ fun BunkerMultiEventHomeScreen( } } else { val type = BunkerRequestUtils.getTypeFromBunker(request.request) - if (request.rememberType.value != RememberType.NEVER && request.checked.value) { + if (rememberType != RememberType.NEVER && isChecked) { AmberUtils.acceptOrRejectPermission( application, localKey, type, null, true, - request.rememberType.value, + rememberType, thisAccount, ) } @@ -527,7 +543,7 @@ fun BunkerMultiEventHomeScreen( type.toString(), null, TimeUtils.now(), - request.checked.value, + isChecked, content = if (type == SignerType.NIP04_DECRYPT || type == SignerType.NIP44_DECRYPT || type == SignerType.DECRYPT_ZAP_EVENT) { request.encryptedData?.result ?: "" } else { @@ -539,7 +555,7 @@ fun BunkerMultiEventHomeScreen( val signature = request.encryptedData?.result ?: continue BunkerRequestUtils.remove(request.request.id) - if (request.checked.value) { + if (isChecked) { BunkerRequestUtils.sendBunkerResponse( context, thisAccount, @@ -572,7 +588,12 @@ fun BunkerMultiEventHomeScreen( } @Composable -private fun BunkerRequestCard(context: Context, bunkerRequest: AmberBunkerRequest) { +private fun BunkerRequestCard( + context: Context, + bunkerRequest: AmberBunkerRequest, + checked: Boolean, + onToggleChecked: () -> Unit, +) { val type = BunkerRequestUtils.getTypeFromBunker(bunkerRequest.request) var showDetails by remember { mutableStateOf(false) } val hasDetails = (type == SignerType.SIGN_EVENT && bunkerRequest.signedEvent != null) || @@ -655,11 +676,11 @@ private fun BunkerRequestCard(context: Context, bunkerRequest: AmberBunkerReques verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable { bunkerRequest.checked.value = !bunkerRequest.checked.value }, + .clickable { onToggleChecked() }, ) { Checkbox( - checked = bunkerRequest.checked.value, - onCheckedChange = { bunkerRequest.checked.value = !bunkerRequest.checked.value }, + checked = checked, + onCheckedChange = { onToggleChecked() }, colors = CheckboxDefaults.colors().copy( uncheckedBorderColor = Color.Gray, ), @@ -671,7 +692,7 @@ private fun BunkerRequestCard(context: Context, bunkerRequest: AmberBunkerReques ) { Text( text = label, - color = if (bunkerRequest.checked.value) Color.Unspecified else Color.Gray, + color = if (checked) Color.Unspecified else Color.Gray, ) if (preview.isNotBlank()) { Text( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerPermissionCard.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerPermissionCard.kt index 7b957aa7..680a0ec0 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerPermissionCard.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerPermissionCard.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -66,14 +67,14 @@ fun BunkerPermissionCard( .fillMaxWidth() .clickable { acceptEventsGroup[index].value = !acceptEventsGroup[index].value - item.second.forEach { it.checked.value = acceptEventsGroup[index].value } + item.second.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = acceptEventsGroup[index].value } }, ) { Checkbox( checked = acceptEventsGroup[index].value, onCheckedChange = { _ -> acceptEventsGroup[index].value = !acceptEventsGroup[index].value - item.second.forEach { it.checked.value = acceptEventsGroup[index].value } + item.second.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = acceptEventsGroup[index].value } }, colors = CheckboxDefaults.colors().copy( uncheckedBorderColor = Color.Gray, @@ -134,7 +135,7 @@ fun BunkerPermissionCard( ) } if (acceptEventsGroup[index].value) { - val selected = item.second.filter { it.checked.value }.size + val selected = item.second.filter { MultiEventScreenIntents.checkedStates[it.request.id] ?: true }.size val total = item.second.size Row( Modifier diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index a707e1be..1b133f7e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -55,6 +55,7 @@ import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind import com.greenart7c3.nostrsigner.service.AmberUtils import com.greenart7c3.nostrsigner.service.ApplicationNameCache +import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType @@ -82,6 +83,12 @@ fun IntentMultiEventHomeScreen( var rememberType by remember { mutableStateOf(RememberType.NEVER) } var relayAuthScope by remember { mutableStateOf(RelayAuthScope.SPECIFIC) } + LaunchedEffect(Unit) { + MultiEventScreenIntents.checkedStates.clear() + MultiEventScreenIntents.rememberType = RememberType.NEVER + intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = true } + } + LaunchedEffect(Unit) { launch(Dispatchers.IO) { localAccount = LocalPreferences.loadFromEncryptedStorage( @@ -124,8 +131,8 @@ fun IntentMultiEventHomeScreen( SigningAs(accountParam) val allCheckedState = when { - intents.all { it.checked.value } -> ToggleableState.On - intents.none { it.checked.value } -> ToggleableState.Off + intents.all { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.On + intents.none { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.Off else -> ToggleableState.Indeterminate } Row( @@ -134,14 +141,14 @@ fun IntentMultiEventHomeScreen( .fillMaxWidth() .clickable { val newValue = allCheckedState != ToggleableState.On - intents.forEach { it.checked.value = newValue } + intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = newValue } }, ) { TriStateCheckbox( state = allCheckedState, onClick = { val newValue = allCheckedState != ToggleableState.On - intents.forEach { it.checked.value = newValue } + intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = newValue } }, ) Text(stringResource(R.string.select_deselect_all)) @@ -153,7 +160,15 @@ fun IntentMultiEventHomeScreen( .verticalScroll(rememberScrollState()), ) { intents.forEach { intent -> - IntentRequestCard(context = context, intent = intent) + IntentRequestCard( + context = context, + intent = intent, + checked = MultiEventScreenIntents.checkedStates[intent.id] ?: true, + onToggleChecked = { + val current = MultiEventScreenIntents.checkedStates[intent.id] ?: true + MultiEventScreenIntents.checkedStates[intent.id] = !current + }, + ) } } @@ -192,9 +207,7 @@ fun IntentMultiEventHomeScreen( onReject = {}, onChanged = { rememberType = it - intents.forEach { intent -> - intent.rememberType.value = rememberType - } + MultiEventScreenIntents.rememberType = it }, packageName = packageName, ) @@ -250,7 +263,8 @@ fun IntentMultiEventHomeScreen( permissions = mutableListOf(), ) - if (intentData.rememberType.value != RememberType.NEVER && intentData.checked.value) { + val isChecked = MultiEventScreenIntents.checkedStates[intentData.id] ?: true + if (rememberType != RememberType.NEVER && isChecked) { val rejectKind = if (intentData.type == SignerType.SIGN_EVENT) intentData.event?.kind else null val rejectRelay = if (intentData.type == SignerType.SIGN_EVENT && intentData.event?.kind == 22242) { if (relayAuthScope == RelayAuthScope.ALL) { @@ -275,7 +289,7 @@ fun IntentMultiEventHomeScreen( intentData.type, rejectKind, false, - intentData.rememberType.value, + rememberType, thisAccount, relay = rejectRelay, ) @@ -356,10 +370,12 @@ fun IntentMultiEventHomeScreen( closeApp = false } + val isChecked = MultiEventScreenIntents.checkedStates[intentData.id] ?: true + if (intentData.type == SignerType.SIGN_EVENT) { val localEvent = intentData.event!! - if (intentData.rememberType.value != RememberType.NEVER && intentData.checked.value) { + if (rememberType != RememberType.NEVER && isChecked) { val signRelay = if (localEvent.kind == 22242) { if (relayAuthScope == RelayAuthScope.ALL) { "*" @@ -383,7 +399,7 @@ fun IntentMultiEventHomeScreen( intentData.type, localEvent.kind, true, - intentData.rememberType.value, + rememberType, thisAccount, relay = signRelay, ) @@ -398,13 +414,13 @@ fun IntentMultiEventHomeScreen( intentData.type.toString(), localEvent.kind, TimeUtils.now(), - intentData.checked.value, + isChecked, content = localEvent.toJson(), ), thisAccount.npub, ) - if (intentData.checked.value) { + if (isChecked) { results.add( Result( null, @@ -432,14 +448,14 @@ fun IntentMultiEventHomeScreen( ) } } else if (intentData.type == SignerType.SIGN_MESSAGE) { - if (intentData.rememberType.value != RememberType.NEVER && intentData.checked.value) { + if (rememberType != RememberType.NEVER && isChecked) { AmberUtils.acceptOrRejectPermission( application, localKey, intentData.type, null, true, - intentData.rememberType.value, + rememberType, thisAccount, ) } @@ -452,14 +468,14 @@ fun IntentMultiEventHomeScreen( intentData.type.toString(), null, TimeUtils.now(), - intentData.checked.value, + isChecked, content = intentData.data, ), thisAccount.npub, ) val signedMessage = thisAccount.signString(intentData.data) - if (intentData.checked.value) { + if (isChecked) { results.add( Result( null, @@ -471,14 +487,14 @@ fun IntentMultiEventHomeScreen( ) } } else { - if (intentData.rememberType.value != RememberType.NEVER && intentData.checked.value) { + if (rememberType != RememberType.NEVER && isChecked) { AmberUtils.acceptOrRejectPermission( application, localKey, intentData.type, null, true, - intentData.rememberType.value, + rememberType, thisAccount, ) } @@ -492,7 +508,7 @@ fun IntentMultiEventHomeScreen( intentData.type.toString(), null, TimeUtils.now(), - intentData.checked.value, + isChecked, content = if (intentData.type == SignerType.NIP04_DECRYPT || intentData.type == SignerType.NIP44_DECRYPT || intentData.type == SignerType.DECRYPT_ZAP_EVENT) { intentData.encryptedData?.result ?: "" } else { @@ -503,7 +519,7 @@ fun IntentMultiEventHomeScreen( ) val signature = intentData.encryptedData?.result ?: continue - if (intentData.checked.value) { + if (isChecked) { results.add( Result( null, @@ -533,7 +549,12 @@ fun IntentMultiEventHomeScreen( } @Composable -private fun IntentRequestCard(context: Context, intent: IntentData) { +private fun IntentRequestCard( + context: Context, + intent: IntentData, + checked: Boolean, + onToggleChecked: () -> Unit, +) { val type = intent.type var showDetails by remember { mutableStateOf(false) } val hasDetails = (type == SignerType.SIGN_EVENT && intent.event != null) || @@ -615,11 +636,11 @@ private fun IntentRequestCard(context: Context, intent: IntentData) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable { intent.checked.value = !intent.checked.value }, + .clickable { onToggleChecked() }, ) { Checkbox( - checked = intent.checked.value, - onCheckedChange = { intent.checked.value = !intent.checked.value }, + checked = checked, + onCheckedChange = { onToggleChecked() }, colors = CheckboxDefaults.colors().copy( uncheckedBorderColor = Color.Gray, ), @@ -631,7 +652,7 @@ private fun IntentRequestCard(context: Context, intent: IntentData) { ) { Text( text = label, - color = if (intent.checked.value) Color.Unspecified else Color.Gray, + color = if (checked) Color.Unspecified else Color.Gray, ) if (preview.isNotBlank()) { Text( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PermissionCard.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PermissionCard.kt index 78c411a6..a12c7fa8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PermissionCard.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PermissionCard.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -66,14 +67,14 @@ fun PermissionCard( .fillMaxWidth() .clickable { acceptEventsGroup[index].value = !acceptEventsGroup[index].value - item.second.forEach { it.checked.value = acceptEventsGroup[index].value } + item.second.forEach { MultiEventScreenIntents.checkedStates[it.id] = acceptEventsGroup[index].value } }, ) { Checkbox( checked = acceptEventsGroup[index].value, onCheckedChange = { _ -> acceptEventsGroup[index].value = !acceptEventsGroup[index].value - item.second.forEach { it.checked.value = acceptEventsGroup[index].value } + item.second.forEach { MultiEventScreenIntents.checkedStates[it.id] = acceptEventsGroup[index].value } }, colors = CheckboxDefaults.colors().copy( uncheckedBorderColor = Color.Gray, @@ -134,7 +135,7 @@ fun PermissionCard( ) } if (acceptEventsGroup[index].value) { - val selected = item.second.filter { it.checked.value }.size + val selected = item.second.filter { MultiEventScreenIntents.checkedStates[it.id] ?: true }.size val total = item.second.size Row( Modifier From 8bee21a281a2bfdc9747c0cedeb26ed265e27256 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 11:52:48 +0000 Subject: [PATCH 2/6] Batch select/deselect all updates to avoid N recompositions Replace per-item forEach writes with a single putAll() call on the SnapshotStateMap so all checked-state changes are applied in one snapshot transaction, triggering a single recomposition instead of one per item. https://claude.ai/code/session_01PRSKjmFms1Tj1QJ5FhfqZm --- .../nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt | 4 ++-- .../nostrsigner/ui/components/IntentMultiEventHomeScreen.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index 716dd559..3dce4f41 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -133,14 +133,14 @@ fun BunkerMultiEventHomeScreen( .fillMaxWidth() .clickable { val newValue = allCheckedState != ToggleableState.On - bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = newValue } + MultiEventScreenIntents.checkedStates.putAll(bunkerRequests.associate { it.request.id to newValue }) }, ) { TriStateCheckbox( state = allCheckedState, onClick = { val newValue = allCheckedState != ToggleableState.On - bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = newValue } + MultiEventScreenIntents.checkedStates.putAll(bunkerRequests.associate { it.request.id to newValue }) }, ) Text(stringResource(R.string.select_deselect_all)) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index 1b133f7e..d3a3bf50 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -141,14 +141,14 @@ fun IntentMultiEventHomeScreen( .fillMaxWidth() .clickable { val newValue = allCheckedState != ToggleableState.On - intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = newValue } + MultiEventScreenIntents.checkedStates.putAll(intents.associate { it.id to newValue }) }, ) { TriStateCheckbox( state = allCheckedState, onClick = { val newValue = allCheckedState != ToggleableState.On - intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = newValue } + MultiEventScreenIntents.checkedStates.putAll(intents.associate { it.id to newValue }) }, ) Text(stringResource(R.string.select_deselect_all)) From 62fa6eb36832567249a2cce30fd3fefd853b952f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 11:54:22 +0000 Subject: [PATCH 3/6] Annotate IntentData and AmberBunkerRequest with @Immutable Tells the Compose compiler these data classes are stable so that composables receiving List or List can be skipped during recomposition when the list reference hasn't changed, avoiding the 'unstable parameter' performance warning. https://claude.ai/code/session_01PRSKjmFms1Tj1QJ5FhfqZm --- .../com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt | 2 ++ .../main/java/com/greenart7c3/nostrsigner/models/IntentData.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt index 8d640df9..75aab8d5 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt @@ -1,5 +1,6 @@ package com.greenart7c3.nostrsigner.models +import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest @@ -9,6 +10,7 @@ enum class EncryptionType { NIP04, } +@Immutable data class AmberBunkerRequest( val request: BunkerRequest, val localKey: String, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt index fbc463e8..fde5aaf9 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/IntentData.kt @@ -1,8 +1,10 @@ package com.greenart7c3.nostrsigner.models +import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +@Immutable data class IntentData( val data: String, val name: String, From 3b66e5b3c10a5db04df7ef463128680bf861bcde Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 11:58:28 +0000 Subject: [PATCH 4/6] Replace Column+verticalScroll with LazyColumn for request cards With Column, all N request cards are composed simultaneously regardless of visibility. With LazyColumn, only visible cards (~5-10) are in the composition tree, so select/deselect all only recomposes visible items instead of every item in the list. https://claude.ai/code/session_01PRSKjmFms1Tj1QJ5FhfqZm --- .../ui/components/BunkerMultiEventHomeScreen.kt | 12 +++++------- .../ui/components/IntentMultiEventHomeScreen.kt | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index 3dce4f41..f5136f8c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -9,8 +9,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -146,12 +146,10 @@ fun BunkerMultiEventHomeScreen( Text(stringResource(R.string.select_deselect_all)) } - Column( - Modifier - .weight(1f) - .verticalScroll(rememberScrollState()), + LazyColumn( + Modifier.weight(1f), ) { - bunkerRequests.forEach { bunkerRequest -> + items(bunkerRequests, key = { it.request.id }) { bunkerRequest -> BunkerRequestCard( context = context, bunkerRequest = bunkerRequest, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index d3a3bf50..2751b7b9 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -154,12 +154,10 @@ fun IntentMultiEventHomeScreen( Text(stringResource(R.string.select_deselect_all)) } - Column( - Modifier - .weight(1f) - .verticalScroll(rememberScrollState()), + LazyColumn( + Modifier.weight(1f), ) { - intents.forEach { intent -> + items(intents, key = { it.id }) { intent -> IntentRequestCard( context = context, intent = intent, From 91aadf89ae1a86fbcb3b08ce7c0e758a4d1f82e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 12:46:57 +0000 Subject: [PATCH 5/6] Use ImmutableList for request list parameters through the composable chain Plain List has runtime-determined stability in Compose, preventing compile-time skip optimizations. Switching to ImmutableList (which is @Immutable-annotated) lets the compiler guarantee that composables receiving unchanged list references can be safely skipped. Also fix redundant smart-cast in BunkerMultiEventHomeScreen. https://claude.ai/code/session_01PRSKjmFms1Tj1QJ5FhfqZm --- .../java/com/greenart7c3/nostrsigner/MainActivity.kt | 3 ++- .../java/com/greenart7c3/nostrsigner/MainViewModel.kt | 11 +++++++---- .../com/greenart7c3/nostrsigner/SignerActivity.kt | 3 ++- .../nostrsigner/service/BunkerRequestUtils.kt | 11 +++++++---- .../com/greenart7c3/nostrsigner/ui/AccountScreen.kt | 6 ++++-- .../nostrsigner/ui/IncomingRequestScreen.kt | 5 +++-- .../java/com/greenart7c3/nostrsigner/ui/MainScreen.kt | 5 +++-- .../nostrsigner/ui/components/AmberTopAppBar.kt | 5 +++-- .../ui/components/BunkerMultiEventHomeScreen.kt | 5 +++-- .../nostrsigner/ui/components/BunkerPermissionCard.kt | 2 +- .../ui/components/IntentMultiEventHomeScreen.kt | 3 ++- .../nostrsigner/ui/components/MultiEventHomeScreen.kt | 5 +++-- .../nostrsigner/ui/components/PermissionCard.kt | 2 +- 13 files changed, 41 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt b/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt index f86933f7..e2e84b7b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.Hex import java.time.Duration import java.time.Instant +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -110,7 +111,7 @@ class MainActivity : AppCompatActivity() { CircularProgressIndicator() } } else { - val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(emptyList()) + val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(persistentListOf()) val npub = remember { mainViewModel.getAccount(intent?.getStringExtra("current_user")) } val accountStateViewModel: AccountStateViewModel = diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/MainViewModel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/MainViewModel.kt index abc98996..a3d5d6f3 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/MainViewModel.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/MainViewModel.kt @@ -16,6 +16,9 @@ import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.Hex +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -24,21 +27,21 @@ import kotlinx.coroutines.launch @SuppressLint("StaticFieldLeak") class MainViewModel(val context: Context) : ViewModel() { - private val _intents = MutableStateFlow>(listOf()) + private val _intents = MutableStateFlow>(persistentListOf()) val intents = _intents.asStateFlow() var navController: NavHostController? = null fun addAll(list: List) { val newList = list.filter { !intents.value.contains(it) } - _intents.value += newList + _intents.value = (_intents.value + newList).toPersistentList() } fun removeAll(intents: List) { - _intents.value -= intents.toSet() + _intents.value = (_intents.value - intents.toSet()).toPersistentList() } fun clear() { - _intents.value = emptyList() + _intents.value = persistentListOf() } fun getAccount(userFromIntent: String?): String? { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerActivity.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerActivity.kt index 496483a3..70d73efd 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerActivity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerActivity.kt @@ -43,6 +43,7 @@ import com.greenart7c3.nostrsigner.ui.navigation.Route import com.greenart7c3.nostrsigner.ui.theme.NostrSignerTheme import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.Hex +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -126,7 +127,7 @@ class SignerActivity : AppCompatActivity() { CircularProgressIndicator() } } else { - val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(emptyList()) + val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(persistentListOf()) val npub = remember { mainViewModel.getAccount(intent?.getStringExtra("current_user")) } val accountStateViewModel: AccountStateViewModel = diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt index 85dd709a..16089ce0 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt @@ -28,25 +28,28 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.toSet +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch object BunkerRequestUtils { - val state = MutableStateFlow(listOf()) + val state = MutableStateFlow>(persistentListOf()) fun addRequest(request: AmberBunkerRequest) { if (state.value.any { it.request.id == request.request.id }) return - state.tryEmit(state.value + request) + state.tryEmit((state.value + request).toPersistentList()) } fun clearRequests() { - state.tryEmit(emptyList()) + state.tryEmit(persistentListOf()) } fun remove(id: String) { - state.tryEmit(state.value.filter { it.request.id != id }) + state.tryEmit(state.value.filter { it.request.id != id }.toPersistentList()) } fun getBunkerRequests(): List = state.value diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt index c6bbd799..6c56b270 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt @@ -38,6 +38,8 @@ import com.greenart7c3.nostrsigner.models.AmberBunkerRequest import com.greenart7c3.nostrsigner.models.IntentResultType import com.greenart7c3.nostrsigner.service.IntentUtils import com.greenart7c3.nostrsigner.ui.navigation.Route +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @SuppressLint("StateFlowValueCalledInComposition", "UnrememberedMutableState") @Composable @@ -47,7 +49,7 @@ fun AccountScreen( packageName: String?, appName: String?, mainViewModel: MainViewModel, - bunkerRequests: List, + bunkerRequests: ImmutableList, navController: NavHostController, isExternalRequest: Boolean = false, ) { @@ -66,7 +68,7 @@ fun AccountScreen( MainLoginPage(accountStateViewModel, newNavController) } is AccountState.LoggedIn -> { - val intents by mainViewModel.intents.collectAsState(initial = emptyList()) + val intents by mainViewModel.intents.collectAsState(initial = persistentListOf()) LaunchedEffect(intent) { intent?.let { IntentUtils.getIntentData( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/IncomingRequestScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/IncomingRequestScreen.kt index 24a03491..185c7664 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/IncomingRequestScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/IncomingRequestScreen.kt @@ -26,12 +26,13 @@ import com.greenart7c3.nostrsigner.models.IntentResultType import com.greenart7c3.nostrsigner.ui.components.BunkerSingleEventHomeScreen import com.greenart7c3.nostrsigner.ui.components.IntentSingleEventHomeScreen import com.greenart7c3.nostrsigner.ui.components.MultiEventHomeScreen +import kotlinx.collections.immutable.ImmutableList @Composable fun IncomingRequestScreen( modifier: Modifier, - intents: List, - bunkerRequests: List, + intents: ImmutableList, + bunkerRequests: ImmutableList, packageName: String?, applicationName: String?, account: Account, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index 9c040705..9382ae9c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -81,6 +81,7 @@ import com.greenart7c3.nostrsigner.ui.components.AmberFloatingButton import com.greenart7c3.nostrsigner.ui.components.AmberTopAppBar import com.greenart7c3.nostrsigner.ui.navigation.Route import java.util.Base64 +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -131,8 +132,8 @@ fun requestIgnoreBatteryOptimizations(context: Context) { fun MainScreen( account: Account, accountStateViewModel: AccountStateViewModel, - intents: List, - bunkerRequests: List, + intents: ImmutableList, + bunkerRequests: ImmutableList, packageName: String?, appName: String?, route: MutableState, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt index b7b47927..d00e4c16 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt @@ -50,6 +50,7 @@ import com.greenart7c3.nostrsigner.ui.navigation.Route import com.greenart7c3.nostrsigner.ui.navigation.routes import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect import java.util.Base64 +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -63,8 +64,8 @@ fun AmberTopAppBar( context: Context, navBackStackEntry: NavBackStackEntry?, account: Account, - intents: List, - bunkerRequests: List, + intents: ImmutableList, + bunkerRequests: ImmutableList, packageName: String?, ) { if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index f5136f8c..71aaf994 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -64,19 +64,20 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.forEach import kotlin.collections.set +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable fun BunkerMultiEventHomeScreen( modifier: Modifier, - bunkerRequests: List, + bunkerRequests: ImmutableList, packageName: String?, accountParam: Account, onLoading: (Boolean) -> Unit, ) { val context = LocalContext.current - val hasRelayAuthEvents = bunkerRequests.any { it.request is BunkerRequestSign && (it.request as BunkerRequestSign).event.kind == 22242 } + val hasRelayAuthEvents = bunkerRequests.any { it.request is BunkerRequestSign && it.request.event.kind == 22242 } var localAccount by remember { mutableStateOf("") } val key = bunkerRequests.first().localKey var rememberType by remember { mutableStateOf(RememberType.NEVER) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerPermissionCard.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerPermissionCard.kt index 680a0ec0..8390e30c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerPermissionCard.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerPermissionCard.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,6 +38,7 @@ import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind +import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import kotlin.collections.forEach @Composable diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index 2751b7b9..539fba95 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -64,13 +64,14 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.forEach import kotlin.collections.set +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable fun IntentMultiEventHomeScreen( modifier: Modifier, - intents: List, + intents: ImmutableList, packageName: String?, accountParam: Account, onRemoveIntentData: (List, IntentResultType) -> Unit, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt index 8e0934fd..347d52b8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt @@ -6,12 +6,13 @@ import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.AmberBunkerRequest import com.greenart7c3.nostrsigner.models.IntentData import com.greenart7c3.nostrsigner.models.IntentResultType +import kotlinx.collections.immutable.ImmutableList @Composable fun MultiEventHomeScreen( modifier: Modifier, - intents: List, - bunkerRequests: List, + intents: ImmutableList, + bunkerRequests: ImmutableList, packageName: String?, accountParam: Account, onRemoveIntentData: (List, IntentResultType) -> Unit, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PermissionCard.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PermissionCard.kt index a12c7fa8..73dff192 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PermissionCard.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PermissionCard.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,6 +38,7 @@ import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind +import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import kotlin.collections.forEach @Composable From a8c39179e32814bbb8094e8625b330b68d79b836 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 20 Mar 2026 10:03:42 -0300 Subject: [PATCH 6/6] Clean up unused state and logic in IntentMultiEventHomeScreen - Remove unused `localAccount` and `appName` state variables along with their corresponding `LaunchedEffect` blocks that were fetching application names from the database and cache. - Remove several unused imports, including `ApplicationNameCache` and `AmberUtils`. --- .../components/IntentMultiEventHomeScreen.kt | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index 539fba95..5ac7e083 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -54,16 +54,12 @@ import com.greenart7c3.nostrsigner.models.Result import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind import com.greenart7c3.nostrsigner.service.AmberUtils -import com.greenart7c3.nostrsigner.service.ApplicationNameCache import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import com.greenart7c3.nostrsigner.service.model.AmberEvent -import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType import com.greenart7c3.nostrsigner.ui.theme.orange import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.forEach -import kotlin.collections.set import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -79,8 +75,6 @@ fun IntentMultiEventHomeScreen( ) { val context = LocalContext.current val hasRelayAuthEvents = intents.any { it.type == SignerType.SIGN_EVENT && it.event?.kind == 22242 } - var localAccount by remember { mutableStateOf("") } - val key = "$packageName" var rememberType by remember { mutableStateOf(RememberType.NEVER) } var relayAuthScope by remember { mutableStateOf(RelayAuthScope.SPECIFIC) } @@ -90,33 +84,6 @@ fun IntentMultiEventHomeScreen( intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = true } } - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - localAccount = LocalPreferences.loadFromEncryptedStorage( - context, - intents.firstOrNull()?.currentAccount ?: "", - )?.npub?.toShortenHex() ?: "" - } - } - - var appName by remember { mutableStateOf(ApplicationNameCache.names["$localAccount-$key"] ?: key.toShortenHex()) } - - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - if (ApplicationNameCache.names["$localAccount-$key"] == null) { - val app = Amber.instance.getDatabase(accountParam.npub).dao().getByKey(key) - app?.let { - appName = it.application.name - ApplicationNameCache.names["$localAccount-$key"] = it.application.name - } - } else { - ApplicationNameCache.names["$localAccount-$key"]?.let { - appName = it - } - } - } - } - Column( modifier, ) {