From 3e1d5579a7c5c09f97a15e700cef12736ee33ca8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 13:37:35 +0000 Subject: [PATCH 1/3] Add per-group remember and scope options to the grouped events UI Replace the single global remember selector and relay-auth toggle in the multi-event approval screens with per-type-group options: each request group now has its own remember selector plus its type-specific scope toggle (relay-auth scope for kind 22242, encryption scope for NIP-04/NIP-44 encrypt/decrypt, kind scope for NIP-44 v3). Approve and discard handlers look up the selections per group and now pass the decrypt type scope (and the v3 kind when scoped) when persisting permissions, matching the single-event screens. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01REtTRsYA3aUGpmbjENbNVb --- .../components/BunkerMultiEventHomeScreen.kt | 96 +++++++------- .../components/IntentMultiEventHomeScreen.kt | 80 ++++++------ .../ui/components/RememberMyChoice.kt | 37 ++++-- .../ui/components/RequestGroupUtils.kt | 121 ++++++++++++++++++ .../ui/components/RequestGroupUtilsTest.kt | 29 +++++ 5 files changed, 264 insertions(+), 99 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 399700fd..03c0e2be 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 @@ -78,11 +78,11 @@ fun BunkerMultiEventHomeScreen( onLoading: (Boolean) -> Unit, ) { val context = LocalContext.current - 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) } - var relayAuthScope by remember { mutableStateOf(RelayAuthScope.SPECIFIC) } + val groupRememberTypes = remember { mutableStateMapOf() } + val groupRelayAuthScopes = remember { mutableStateMapOf() } + val groupDecryptScopes = remember { mutableStateMapOf() } var appName by remember { mutableStateOf(ApplicationNameCache["$localAccount-$key"] ?: key.toShortenHex()) } var appIcon by remember { mutableStateOf(bunkerRequests.first().clientMetadata?.image ?: "") } @@ -202,45 +202,23 @@ fun BunkerMultiEventHomeScreen( }, ) } + if (groupKey.hasGroupOptions()) { + item(key = "group-options:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { + RequestGroupOptions( + groupKey = groupKey, + rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER, + onRememberTypeChanged = { groupRememberTypes[groupKey] = it }, + relayAuthScope = groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC, + onRelayAuthScopeChanged = { groupRelayAuthScopes[groupKey] = it }, + decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(groupKey.type), + onDecryptTypeScopeChanged = { groupDecryptScopes[groupKey] = it }, + ) + } + } } } } - if (hasRelayAuthEvents) { - LabeledBorderBox( - label = stringResource(R.string.relay_auth_scope), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 4.dp, vertical = 8.dp), - ) { - AmberToggles( - selected = relayAuthScope, - options = listOf(RelayAuthScope.SPECIFIC, RelayAuthScope.ALL), - onSelected = { relayAuthScope = it }, - label = { - stringResource( - when (it) { - RelayAuthScope.SPECIFIC -> R.string.for_this_relay_only - RelayAuthScope.ALL -> R.string.for_all_relays - }, - ) - }, - ) - } - } - - RememberMyChoice( - alwaysShow = true, - shouldRunAcceptOrReject = null, - onAccept = {}, - onReject = {}, - onChanged = { - rememberType = it - MultiEventScreenIntents.rememberType = it - }, - packageName = packageName, - ) - Row( Modifier .fillMaxWidth() @@ -297,10 +275,24 @@ fun BunkerMultiEventHomeScreen( ) val isChecked = MultiEventScreenIntents.checkedStates[request.request.id] ?: true + val requestType = BunkerRequestUtils.getTypeFromBunker(request.request) + val groupKey = requestGroupKey( + type = requestType, + eventKind = (request.request as? BunkerRequestSign)?.event?.kind, + encryptedData = request.encryptedData, + nip44v3Kind = BunkerRequestUtils.getNip44v3Kind(request.request), + ) + val rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER if (rememberType != RememberType.NEVER && isChecked) { - val rejectKind = if (request.request is BunkerRequestSign) request.request.event.kind else null + val decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(requestType) + val rejectKind = when { + request.request is BunkerRequestSign -> request.request.event.kind + requestType == SignerType.NIP44_V3_ENCRYPT || requestType == SignerType.NIP44_V3_DECRYPT -> + if (decryptTypeScope == DecryptTypeScope.SPECIFIC) BunkerRequestUtils.getNip44v3Kind(request.request) else null + else -> null + } val rejectRelay = if (request.request is BunkerRequestSign && request.request.event.kind == 22242) { - if (relayAuthScope == RelayAuthScope.ALL) { + if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) { "*" } else { RelayUrlUtils.extractHostAndPort(AmberEvent.relay(request.request.event)) @@ -311,13 +303,14 @@ fun BunkerMultiEventHomeScreen( AmberUtils.acceptOrRejectPermission( application, localKey, - BunkerRequestUtils.getTypeFromBunker(request.request), + requestType, rejectKind, false, rememberType, thisAccount, relay = rejectRelay, encryptedData = request.encryptedData, + decryptTypeScope = decryptTypeScope, ) } @@ -389,13 +382,21 @@ fun BunkerMultiEventHomeScreen( ) val isChecked = MultiEventScreenIntents.checkedStates[request.request.id] ?: true + val requestType = BunkerRequestUtils.getTypeFromBunker(request.request) + val groupKey = requestGroupKey( + type = requestType, + eventKind = (request.request as? BunkerRequestSign)?.event?.kind, + encryptedData = request.encryptedData, + nip44v3Kind = BunkerRequestUtils.getNip44v3Kind(request.request), + ) + val rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER if (request.request is BunkerRequestSign) { val localEvent = request.signedEvent!! if (rememberType != RememberType.NEVER && isChecked) { val signRelay = if (localEvent.kind == 22242) { - if (relayAuthScope == RelayAuthScope.ALL) { + if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) { "*" } else { RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent)) @@ -497,17 +498,24 @@ fun BunkerMultiEventHomeScreen( } } } else { - val type = BunkerRequestUtils.getTypeFromBunker(request.request) + val type = requestType if (rememberType != RememberType.NEVER && isChecked) { + val decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(type) + val permissionKind = if (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) { + if (decryptTypeScope == DecryptTypeScope.SPECIFIC) BunkerRequestUtils.getNip44v3Kind(request.request) else null + } else { + null + } AmberUtils.acceptOrRejectPermission( application, localKey, type, - null, + permissionKind, true, rememberType, thisAccount, encryptedData = request.encryptedData, + decryptTypeScope = decryptTypeScope, ) } 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 132ac5c0..c1966970 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 @@ -75,9 +75,9 @@ fun IntentMultiEventHomeScreen( onLoading: (Boolean) -> Unit, ) { val context = LocalContext.current - val hasRelayAuthEvents = intents.any { it.type == SignerType.SIGN_EVENT && it.event?.kind == 22242 } - var rememberType by remember { mutableStateOf(RememberType.NEVER) } - var relayAuthScope by remember { mutableStateOf(RelayAuthScope.SPECIFIC) } + val groupRememberTypes = remember { mutableStateMapOf() } + val groupRelayAuthScopes = remember { mutableStateMapOf() } + val groupDecryptScopes = remember { mutableStateMapOf() } LaunchedEffect(Unit) { MultiEventScreenIntents.checkedStates.clear() @@ -168,45 +168,23 @@ fun IntentMultiEventHomeScreen( }, ) } + if (groupKey.hasGroupOptions()) { + item(key = "group-options:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { + RequestGroupOptions( + groupKey = groupKey, + rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER, + onRememberTypeChanged = { groupRememberTypes[groupKey] = it }, + relayAuthScope = groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC, + onRelayAuthScopeChanged = { groupRelayAuthScopes[groupKey] = it }, + decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(groupKey.type), + onDecryptTypeScopeChanged = { groupDecryptScopes[groupKey] = it }, + ) + } + } } } } - if (hasRelayAuthEvents) { - LabeledBorderBox( - label = stringResource(R.string.relay_auth_scope), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 4.dp, vertical = 8.dp), - ) { - AmberToggles( - selected = relayAuthScope, - options = listOf(RelayAuthScope.SPECIFIC, RelayAuthScope.ALL), - onSelected = { relayAuthScope = it }, - label = { - stringResource( - when (it) { - RelayAuthScope.SPECIFIC -> R.string.for_this_relay_only - RelayAuthScope.ALL -> R.string.for_all_relays - }, - ) - }, - ) - } - } - - RememberMyChoice( - alwaysShow = true, - shouldRunAcceptOrReject = null, - onAccept = {}, - onReject = {}, - onChanged = { - rememberType = it - MultiEventScreenIntents.rememberType = it - }, - packageName = packageName, - ) - Row( Modifier .fillMaxWidth() @@ -260,10 +238,18 @@ fun IntentMultiEventHomeScreen( var permissionsChanged = false for (intentData in accountIntents) { val isChecked = MultiEventScreenIntents.checkedStates[intentData.id] ?: true + val groupKey = requestGroupKey(intentData.type, intentData.event?.kind, intentData.encryptedData, intentData.nip44v3Kind) + val rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER if (rememberType != RememberType.NEVER && isChecked) { - val rejectKind = if (intentData.type == SignerType.SIGN_EVENT) intentData.event?.kind else null + val decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(intentData.type) + val rejectKind = when { + intentData.type == SignerType.SIGN_EVENT -> intentData.event?.kind + intentData.type == SignerType.NIP44_V3_ENCRYPT || intentData.type == SignerType.NIP44_V3_DECRYPT -> + if (decryptTypeScope == DecryptTypeScope.SPECIFIC) intentData.nip44v3Kind else null + else -> null + } val rejectRelay = if (intentData.type == SignerType.SIGN_EVENT && intentData.event?.kind == 22242) { - if (relayAuthScope == RelayAuthScope.ALL) { + if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) { "*" } else { RelayUrlUtils.extractHostAndPort(AmberEvent.relay(intentData.event)) @@ -280,6 +266,7 @@ fun IntentMultiEventHomeScreen( rememberType, relay = rejectRelay, encryptedData = intentData.encryptedData, + decryptTypeScope = decryptTypeScope, ) permissionsChanged = true } @@ -359,13 +346,15 @@ fun IntentMultiEventHomeScreen( for (intentData in accountIntents) { val isChecked = MultiEventScreenIntents.checkedStates[intentData.id] ?: true val type = intentData.type + val groupKey = requestGroupKey(type, intentData.event?.kind, intentData.encryptedData, intentData.nip44v3Kind) + val rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER if (type == SignerType.SIGN_EVENT) { val localEvent = intentData.event!! if (rememberType != RememberType.NEVER && isChecked) { val signRelay = if (localEvent.kind == 22242) { - if (relayAuthScope == RelayAuthScope.ALL) { + if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) { "*" } else { RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent)) @@ -420,14 +409,21 @@ fun IntentMultiEventHomeScreen( } } else { if (rememberType != RememberType.NEVER && isChecked) { + val decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(type) + val permissionKind = if (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) { + if (decryptTypeScope == DecryptTypeScope.SPECIFIC) intentData.nip44v3Kind else null + } else { + null + } AmberUtils.updatePermission( application, localKey, type, - null, + permissionKind, true, rememberType, encryptedData = intentData.encryptedData, + decryptTypeScope = decryptTypeScope, ) permissionsChanged = true } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt index b8b07ff6..093c42ec 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt @@ -57,6 +57,23 @@ fun LabeledBorderBox( } } +@Composable +fun RememberMyChoiceToggles( + selected: RememberType, + onSelected: (RememberType) -> Unit, +) { + LabeledBorderBox( + label = stringResource(R.string.automatically_sign_this_for), + ) { + AmberToggles( + selected = selected, + options = rememberTypeDisplayOrder, + onSelected = onSelected, + label = { stringResource(it.shortResourceId) }, + ) + } +} + @Composable fun RememberMyChoice( shouldRunAcceptOrReject: Boolean?, @@ -77,18 +94,12 @@ fun RememberMyChoice( } } if (packageName != null || alwaysShow) { - LabeledBorderBox( - label = stringResource(R.string.automatically_sign_this_for), - ) { - AmberToggles( - selected = selected, - options = rememberTypeDisplayOrder, - onSelected = { - selected = it - onChanged(it) - }, - label = { stringResource(it.shortResourceId) }, - ) - } + RememberMyChoiceToggles( + selected = selected, + onSelected = { + selected = it + onChanged(it) + }, + ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt index 71171be3..16f06fce 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt @@ -3,6 +3,7 @@ package com.greenart7c3.nostrsigner.ui.components import android.content.Context import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -17,6 +18,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.stringResource import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp import com.greenart7c3.nostrsigner.R @@ -28,6 +30,7 @@ import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind import com.greenart7c3.nostrsigner.models.encryptDecryptSignerTypes +import com.greenart7c3.nostrsigner.ui.RememberType enum class RequestPayloadShape { EVENT, @@ -109,6 +112,124 @@ fun RequestGroupKey.toLabel(context: Context): String { } } +enum class RequestGroupScopeKind { + NONE, + RELAY_AUTH, + ENCRYPTION_METHOD, + NIP44_V3_KIND, +} + +fun RequestGroupKey.scopeKind(): RequestGroupScopeKind = when { + type == SignerType.SIGN_EVENT && kind == 22242 -> RequestGroupScopeKind.RELAY_AUTH + type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT -> RequestGroupScopeKind.NIP44_V3_KIND + type in encryptDecryptSignerTypes && type != SignerType.DECRYPT_ZAP_EVENT -> RequestGroupScopeKind.ENCRYPTION_METHOD + else -> RequestGroupScopeKind.NONE +} + +// Mirrors the single-event defaults: EncryptDecryptData defaults to ALL, Nip44v3ApprovalData to SPECIFIC +fun defaultDecryptTypeScope(type: SignerType): DecryptTypeScope = if (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) { + DecryptTypeScope.SPECIFIC +} else { + DecryptTypeScope.ALL +} + +// CONNECT / GET_PUBLIC_KEY approvals never persist a permission from the grouped flow +fun RequestGroupKey.hasGroupOptions(): Boolean = type != SignerType.CONNECT && type != SignerType.GET_PUBLIC_KEY + +@Composable +fun RequestGroupOptions( + groupKey: RequestGroupKey, + rememberType: RememberType, + onRememberTypeChanged: (RememberType) -> Unit, + relayAuthScope: RelayAuthScope, + onRelayAuthScopeChanged: (RelayAuthScope) -> Unit, + decryptTypeScope: DecryptTypeScope, + onDecryptTypeScopeChanged: (DecryptTypeScope) -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 4.dp), + ) { + when (groupKey.scopeKind()) { + RequestGroupScopeKind.RELAY_AUTH -> { + LabeledBorderBox( + label = stringResource(R.string.relay_auth_scope), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + AmberToggles( + selected = relayAuthScope, + options = listOf(RelayAuthScope.SPECIFIC, RelayAuthScope.ALL), + onSelected = onRelayAuthScopeChanged, + label = { + stringResource( + when (it) { + RelayAuthScope.SPECIFIC -> R.string.for_this_relay_only + RelayAuthScope.ALL -> R.string.for_all_relays + }, + ) + }, + ) + } + } + RequestGroupScopeKind.ENCRYPTION_METHOD -> { + LabeledBorderBox( + label = stringResource(R.string.encryption_scope), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + AmberToggles( + selected = decryptTypeScope, + options = listOf(DecryptTypeScope.SPECIFIC, DecryptTypeScope.ALL), + onSelected = onDecryptTypeScopeChanged, + label = { + stringResource( + when (it) { + DecryptTypeScope.SPECIFIC -> R.string.for_this_method_only + DecryptTypeScope.ALL -> R.string.for_all_methods + }, + ) + }, + ) + } + } + RequestGroupScopeKind.NIP44_V3_KIND -> { + if (groupKey.kind != null) { + LabeledBorderBox( + label = stringResource(R.string.encryption_scope), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + AmberToggles( + selected = decryptTypeScope, + options = listOf(DecryptTypeScope.SPECIFIC, DecryptTypeScope.ALL), + onSelected = onDecryptTypeScopeChanged, + label = { + stringResource( + when (it) { + DecryptTypeScope.SPECIFIC -> R.string.for_this_kind_only + DecryptTypeScope.ALL -> R.string.for_all_kinds + }, + ) + }, + ) + } + } + } + RequestGroupScopeKind.NONE -> {} + } + + RememberMyChoiceToggles( + selected = rememberType, + onSelected = onRememberTypeChanged, + ) + } +} + @Composable fun RequestGroupHeader( label: String, diff --git a/app/src/test/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtilsTest.kt b/app/src/test/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtilsTest.kt index a40f10e1..ed524b4e 100644 --- a/app/src/test/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtilsTest.kt +++ b/app/src/test/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtilsTest.kt @@ -99,6 +99,35 @@ class RequestGroupUtilsTest { ) } + @Test + fun `scopeKind maps relay auth encryption and v3 groups`() { + assertEquals(RequestGroupScopeKind.RELAY_AUTH, RequestGroupKey(SignerType.SIGN_EVENT, 22242, null).scopeKind()) + assertEquals(RequestGroupScopeKind.NONE, RequestGroupKey(SignerType.SIGN_EVENT, 1, null).scopeKind()) + assertEquals(RequestGroupScopeKind.ENCRYPTION_METHOD, RequestGroupKey(SignerType.NIP04_ENCRYPT, null, RequestPayloadShape.CLEAR_TEXT).scopeKind()) + assertEquals(RequestGroupScopeKind.ENCRYPTION_METHOD, RequestGroupKey(SignerType.NIP44_DECRYPT, 4, RequestPayloadShape.EVENT).scopeKind()) + assertEquals(RequestGroupScopeKind.NIP44_V3_KIND, RequestGroupKey(SignerType.NIP44_V3_ENCRYPT, 9, null).scopeKind()) + assertEquals(RequestGroupScopeKind.NIP44_V3_KIND, RequestGroupKey(SignerType.NIP44_V3_DECRYPT, null, null).scopeKind()) + assertEquals(RequestGroupScopeKind.NONE, RequestGroupKey(SignerType.DECRYPT_ZAP_EVENT, null, RequestPayloadShape.PRIVATE_ZAP).scopeKind()) + assertEquals(RequestGroupScopeKind.NONE, RequestGroupKey(SignerType.CONNECT, null, null).scopeKind()) + } + + @Test + fun `defaultDecryptTypeScope is SPECIFIC for v3 and ALL otherwise`() { + assertEquals(DecryptTypeScope.SPECIFIC, defaultDecryptTypeScope(SignerType.NIP44_V3_ENCRYPT)) + assertEquals(DecryptTypeScope.SPECIFIC, defaultDecryptTypeScope(SignerType.NIP44_V3_DECRYPT)) + assertEquals(DecryptTypeScope.ALL, defaultDecryptTypeScope(SignerType.NIP04_DECRYPT)) + assertEquals(DecryptTypeScope.ALL, defaultDecryptTypeScope(SignerType.NIP44_ENCRYPT)) + assertEquals(DecryptTypeScope.ALL, defaultDecryptTypeScope(SignerType.DECRYPT_ZAP_EVENT)) + } + + @Test + fun `hasGroupOptions excludes connect and get public key`() { + assertEquals(false, RequestGroupKey(SignerType.CONNECT, null, null).hasGroupOptions()) + assertEquals(false, RequestGroupKey(SignerType.GET_PUBLIC_KEY, null, null).hasGroupOptions()) + assertEquals(true, RequestGroupKey(SignerType.SIGN_EVENT, 1, null).hasGroupOptions()) + assertEquals(true, RequestGroupKey(SignerType.NIP44_DECRYPT, null, RequestPayloadShape.CLEAR_TEXT).hasGroupOptions()) + } + @Test fun `groupRequests preserves input order within a group`() { data class Item(val id: String, val kind: Int) From aa7262e69e0258e1287facb0adf7c9a902405850 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 14:02:55 +0000 Subject: [PATCH 2/3] Show group options above the request cards, even when collapsed Move each group's options panel to render right under the group header, outside the expanded check, so the remember and scope selectors are always visible without expanding the group or scrolling past its cards. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01REtTRsYA3aUGpmbjENbNVb --- .../components/BunkerMultiEventHomeScreen.kt | 26 +++++++++---------- .../components/IntentMultiEventHomeScreen.kt | 26 +++++++++---------- 2 files changed, 26 insertions(+), 26 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 03c0e2be..fbcb3051 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 @@ -190,6 +190,19 @@ fun BunkerMultiEventHomeScreen( ) } } + if (groupKey.hasGroupOptions()) { + item(key = "group-options:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { + RequestGroupOptions( + groupKey = groupKey, + rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER, + onRememberTypeChanged = { groupRememberTypes[groupKey] = it }, + relayAuthScope = groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC, + onRelayAuthScopeChanged = { groupRelayAuthScopes[groupKey] = it }, + decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(groupKey.type), + onDecryptTypeScopeChanged = { groupDecryptScopes[groupKey] = it }, + ) + } + } if (expanded) { items(groupItems, key = { it.request.id }) { bunkerRequest -> BunkerRequestCard( @@ -202,19 +215,6 @@ fun BunkerMultiEventHomeScreen( }, ) } - if (groupKey.hasGroupOptions()) { - item(key = "group-options:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { - RequestGroupOptions( - groupKey = groupKey, - rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER, - onRememberTypeChanged = { groupRememberTypes[groupKey] = it }, - relayAuthScope = groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC, - onRelayAuthScopeChanged = { groupRelayAuthScopes[groupKey] = it }, - decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(groupKey.type), - onDecryptTypeScopeChanged = { groupDecryptScopes[groupKey] = it }, - ) - } - } } } } 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 c1966970..bb914703 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 @@ -156,6 +156,19 @@ fun IntentMultiEventHomeScreen( ) } } + if (groupKey.hasGroupOptions()) { + item(key = "group-options:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { + RequestGroupOptions( + groupKey = groupKey, + rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER, + onRememberTypeChanged = { groupRememberTypes[groupKey] = it }, + relayAuthScope = groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC, + onRelayAuthScopeChanged = { groupRelayAuthScopes[groupKey] = it }, + decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(groupKey.type), + onDecryptTypeScopeChanged = { groupDecryptScopes[groupKey] = it }, + ) + } + } if (expanded) { items(groupIntents, key = { it.id }) { intent -> IntentRequestCard( @@ -168,19 +181,6 @@ fun IntentMultiEventHomeScreen( }, ) } - if (groupKey.hasGroupOptions()) { - item(key = "group-options:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { - RequestGroupOptions( - groupKey = groupKey, - rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER, - onRememberTypeChanged = { groupRememberTypes[groupKey] = it }, - relayAuthScope = groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC, - onRelayAuthScopeChanged = { groupRelayAuthScopes[groupKey] = it }, - decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(groupKey.type), - onDecryptTypeScopeChanged = { groupDecryptScopes[groupKey] = it }, - ) - } - } } } } From e6a7e6d1af4368890734f7ae2dafad0bb2ddab2f Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 13 Jul 2026 11:24:19 -0300 Subject: [PATCH 3/3] Only use fullscreen layout when there is a single bunker request Co-Authored-By: Claude Opus 4.8 (1M context) --- .../greenart7c3/nostrsigner/ui/MainScreen.kt | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) 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 73c1ff23..7d9434e7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -373,25 +373,14 @@ fun MainScreen( content = { val scrollState = rememberScrollState() - val modifier = if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) { - Modifier - .fillMaxSize() - .padding(padding) - .verticalScrollbar(scrollState) - .verticalScroll(scrollState) - .padding(horizontal = verticalPadding) - .padding(top = verticalPadding * 1.5f) - } else { - Modifier - .fillMaxSize() - .padding(padding) - .verticalScrollbar(scrollState) - .verticalScroll(scrollState) - .padding(horizontal = verticalPadding) - } IncomingRequestScreen( horizontalPadding = verticalPadding, - modifier = modifier, + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScrollbar(scrollState) + .verticalScroll(scrollState) + .padding(horizontal = verticalPadding), scaffoldPadding = padding, intents = intents, bunkerRequests = bunkerRequests,