Merge #7811888b: Redesign multi-request approval screen with Approve/De…

Redesign multi-request approval screen with Approve/Deny toggles (#497)

nostr:nevent1qqs8syvg3ddd7l3cwmfcuwy8f6hdags9usa706kprs506fm9cxsujjcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc06qjx5

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Replace the confusing "select + Approve/Discard" interaction on the
multi-event approval screen with an explicit Approve/Deny model:

- Each request card and each group header now has an Approve/Deny
  segmented toggle (approve = primary, deny = error).
- Removed the top "Approve/Deny all" toggle and the per-group
  TriStateCheckbox; group headers carry their own Approve/Deny toggle.
- Replaced the two bottom buttons (Approve selected / Discard selected)
  with a single Confirm button. Confirm commits every per-request
  decision: approve signs and (when remembered) persists an accept rule;
  deny rejects and (when remembered) persists a deny rule, preserving
  existing deny-always behavior.
- Bunker path now sends a proper bunker error response for denied
  requests instead of leaving the client to time out.
- Relabeled the per-group remember control to "Remember my choice for"
  since it now covers both approve and deny rule persistence.
- Fixed deny toggle contrast: selected text now uses onError for the
  deny segment instead of hardcoded black on dark-red.

AmberToggles gains an optional indicatorColor and selectedTextColor;
ToggleOption takes a selectedTextColor param. Removed unused
discard_all/approve_all strings across all locales.

Closes #497
This commit is contained in:
greenart7c3
2026-07-28 11:39:42 -03:00
20 changed files with 488 additions and 709 deletions
@@ -23,6 +23,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -47,6 +48,8 @@ fun <T> AmberToggles(
onSelected: (T) -> Unit, onSelected: (T) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
label: @Composable (T) -> String, label: @Composable (T) -> String,
indicatorColor: @Composable (T) -> Color = { MaterialTheme.colorScheme.primary },
selectedTextColor: @Composable (T) -> Color = { Color.Black },
) { ) {
val count = options.size.coerceAtLeast(1) val count = options.size.coerceAtLeast(1)
val selectedIndex = options.indexOf(selected).coerceAtLeast(0) val selectedIndex = options.indexOf(selected).coerceAtLeast(0)
@@ -100,7 +103,7 @@ fun <T> AmberToggles(
.width(segmentWidth) .width(segmentWidth)
.fillMaxHeight() .fillMaxHeight()
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.primary), .background(indicatorColor(selected)),
) )
Row(modifier = Modifier.fillMaxHeight()) { Row(modifier = Modifier.fillMaxHeight()) {
@@ -109,6 +112,7 @@ fun <T> AmberToggles(
modifier = Modifier.width(segmentWidth), modifier = Modifier.width(segmentWidth),
text = label(option), text = label(option),
isSelected = option == selected, isSelected = option == selected,
selectedTextColor = selectedTextColor(option),
onClick = { onSelected(option) }, onClick = { onSelected(option) },
) )
} }
@@ -2,22 +2,16 @@ package com.greenart7c3.nostrsigner.ui.components
import android.content.Context import android.content.Context
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -30,7 +24,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.text.toLowerCase
@@ -58,7 +51,6 @@ import com.greenart7c3.nostrsigner.service.RelayUrlUtils
import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.model.AmberEvent
import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.RememberType import com.greenart7c3.nostrsigner.ui.RememberType
import com.greenart7c3.nostrsigner.ui.theme.orange
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
@@ -89,6 +81,7 @@ fun BunkerMultiEventHomeScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
MultiEventScreenIntents.checkedStates.clear() MultiEventScreenIntents.checkedStates.clear()
MultiEventScreenIntents.rememberType = RememberType.NEVER MultiEventScreenIntents.rememberType = RememberType.NEVER
// checkedStates now holds the per-request decision: true = Approve, false = Deny.
bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = true } bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = true }
} }
@@ -128,30 +121,6 @@ fun BunkerMultiEventHomeScreen(
SigningAs(accountParam) SigningAs(accountParam)
val allCheckedState = when {
bunkerRequests.all { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.On
bunkerRequests.none { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.Off
else -> ToggleableState.Indeterminate
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
val newValue = allCheckedState != ToggleableState.On
MultiEventScreenIntents.checkedStates.putAll(bunkerRequests.associate { it.request.id to newValue })
},
) {
TriStateCheckbox(
state = allCheckedState,
onClick = {
val newValue = allCheckedState != ToggleableState.On
MultiEventScreenIntents.checkedStates.putAll(bunkerRequests.associate { it.request.id to newValue })
},
)
Text(stringResource(R.string.select_deselect_all))
}
val groups = remember(bunkerRequests) { val groups = remember(bunkerRequests) {
groupRequests(bunkerRequests) { groupRequests(bunkerRequests) {
requestGroupKey( requestGroupKey(
@@ -170,19 +139,14 @@ fun BunkerMultiEventHomeScreen(
val expanded = groups.size == 1 || (expandedGroups[groupKey] ?: false) val expanded = groups.size == 1 || (expandedGroups[groupKey] ?: false)
if (groups.size > 1) { if (groups.size > 1) {
item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") {
val groupState = when { val groupApproved = groupItems.all { MultiEventScreenIntents.checkedStates[it.request.id] ?: true }
groupItems.all { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.On
groupItems.none { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.Off
else -> ToggleableState.Indeterminate
}
RequestGroupHeader( RequestGroupHeader(
label = groupKey.toLabel(context), label = groupKey.toLabel(context),
count = groupItems.size, count = groupItems.size,
state = groupState, approved = groupApproved,
expanded = expanded, expanded = expanded,
onToggle = { onApproveChanged = { approve ->
val newValue = groupState != ToggleableState.On MultiEventScreenIntents.checkedStates.putAll(groupItems.associate { it.request.id to approve })
MultiEventScreenIntents.checkedStates.putAll(groupItems.associate { it.request.id to newValue })
}, },
onExpandToggle = { onExpandToggle = {
expandedGroups[groupKey] = !(expandedGroups[groupKey] ?: false) expandedGroups[groupKey] = !(expandedGroups[groupKey] ?: false)
@@ -208,10 +172,9 @@ fun BunkerMultiEventHomeScreen(
BunkerRequestCard( BunkerRequestCard(
context = context, context = context,
bunkerRequest = bunkerRequest, bunkerRequest = bunkerRequest,
checked = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true, approved = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true,
onToggleChecked = { onApproveChanged = {
val current = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] = it
MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] = !current
}, },
) )
} }
@@ -219,21 +182,20 @@ fun BunkerMultiEventHomeScreen(
} }
} }
Row( AmberButton(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 16.dp), .padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), text = stringResource(R.string.confirm),
) { onClick = {
AmberButton( onLoading(true)
Modifier.weight(1f), Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
colors = ButtonDefaults.buttonColors().copy( try {
containerColor = orange, reconnectToRelays()
), val closeApp = bunkerRequests.any { it.closeApplication }
onClick = {
Amber.instance.applicationIOScope.launch {
var closeApp = true
BunkerRequestUtils.clearRequests() BunkerRequestUtils.clearRequests()
EventNotificationConsumer(context).notificationManager().cancelAll()
finishActivity(closeApp)
for (request in bunkerRequests) { for (request in bunkerRequests) {
val thisAccount = val thisAccount =
if (request.currentAccount.isNotBlank()) { if (request.currentAccount.isNotBlank()) {
@@ -247,20 +209,23 @@ fun BunkerMultiEventHomeScreen(
val localKey = request.localKey val localKey = request.localKey
val dao = Amber.instance.dao(thisAccount.npub) val dao = Amber.instance.dao(thisAccount.npub)
val historyDatabase = Amber.instance.getHistoryDatabase(thisAccount.npub)
val savedApplication = dao.getByKey(localKey)
val secret = if (request.request is BunkerRequestConnect) { val secret = if (request.request is BunkerRequestConnect) {
request.request.secret ?: "" request.request.secret ?: ""
} else { } else {
"" ""
} }
val application = val application =
dao savedApplication ?: ApplicationWithPermissions(
.getByKey(localKey) ?: ApplicationWithPermissions(
application = ApplicationEntity( application = ApplicationEntity(
localKey, localKey,
"", request.clientMetadata?.name ?: "",
listOf(), listOf(),
"", request.clientMetadata?.url ?: "",
"", request.clientMetadata?.image ?: "",
"", "",
thisAccount.hexKey, thisAccount.hexKey,
true, true,
@@ -274,7 +239,7 @@ fun BunkerMultiEventHomeScreen(
permissions = mutableListOf(), permissions = mutableListOf(),
) )
val isChecked = MultiEventScreenIntents.checkedStates[request.request.id] ?: true val isApproved = MultiEventScreenIntents.checkedStates[request.request.id] ?: true
val requestType = BunkerRequestUtils.getTypeFromBunker(request.request) val requestType = BunkerRequestUtils.getTypeFromBunker(request.request)
val groupKey = requestGroupKey( val groupKey = requestGroupKey(
type = requestType, type = requestType,
@@ -283,165 +248,98 @@ fun BunkerMultiEventHomeScreen(
nip44v3Kind = BunkerRequestUtils.getNip44v3Kind(request.request), nip44v3Kind = BunkerRequestUtils.getNip44v3Kind(request.request),
) )
val rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER val rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER
if (rememberType != RememberType.NEVER && isChecked) {
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 ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) {
"*"
} else {
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(request.request.event))
}
} else {
""
}
AmberUtils.acceptOrRejectPermission(
application,
localKey,
requestType,
rejectKind,
false,
rememberType,
thisAccount,
relay = rejectRelay,
encryptedData = request.encryptedData,
decryptTypeScope = decryptTypeScope,
)
}
if (!application.application.closeApplication) { if (request.request is BunkerRequestSign) {
closeApp = false val localEvent = request.signedEvent!!
}
}
EventNotificationConsumer(context).notificationManager().cancelAll() if (rememberType != RememberType.NEVER) {
finishActivity(closeApp) val signRelay = if (localEvent.kind == 22242) {
} if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) {
}, "*"
text = stringResource(R.string.discard_all),
)
AmberButton(
Modifier.weight(1f),
text = stringResource(R.string.approve_all),
onClick = {
onLoading(true)
Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
try {
reconnectToRelays()
val closeApp = bunkerRequests.any { it.closeApplication }
BunkerRequestUtils.clearRequests()
EventNotificationConsumer(context).notificationManager().cancelAll()
finishActivity(closeApp)
for (request in bunkerRequests) {
val thisAccount =
if (request.currentAccount.isNotBlank()) {
LocalPreferences.loadFromEncryptedStorage(
context,
request.currentAccount,
)
} else {
accountParam
} ?: continue
val localKey = request.localKey
val dao = Amber.instance.dao(thisAccount.npub)
val historyDatabase = Amber.instance.getHistoryDatabase(thisAccount.npub)
val savedApplication = dao.getByKey(localKey)
val secret = if (request.request is BunkerRequestConnect) {
request.request.secret ?: ""
} else {
""
}
val application =
savedApplication ?: ApplicationWithPermissions(
application = ApplicationEntity(
localKey,
request.clientMetadata?.name ?: "",
listOf(),
request.clientMetadata?.url ?: "",
request.clientMetadata?.image ?: "",
"",
thisAccount.hexKey,
true,
secret,
secret.isNotBlank(),
thisAccount.signPolicy,
request.closeApplication,
0L,
lastUsed = TimeUtils.now(),
),
permissions = mutableListOf(),
)
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 ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) {
"*"
} else {
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent))
}
} else { } else {
"" RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent))
} }
AmberUtils.acceptOrRejectPermission( } else {
application = application, ""
key = localKey,
signerType = SignerType.SIGN_EVENT,
kind = localEvent.kind,
value = true,
rememberType = rememberType,
account = thisAccount,
relay = signRelay,
encryptedData = request.encryptedData,
)
} }
AmberUtils.acceptOrRejectPermission(
application = application,
key = localKey,
signerType = SignerType.SIGN_EVENT,
kind = localEvent.kind,
value = isApproved,
rememberType = rememberType,
account = thisAccount,
relay = signRelay,
encryptedData = request.encryptedData,
)
}
dao.insertApplicationWithPermissions(application)
historyDatabase.dao().addHistory(
listOf(
HistoryEntity(
id = 0,
pkKey = localKey,
type = SignerType.SIGN_EVENT.toString(),
kind = localEvent.kind,
time = TimeUtils.now(),
accepted = isApproved,
content = localEvent.toJson(),
),
),
thisAccount.npub,
)
BunkerRequestUtils.remove(request.request.id)
if (isApproved) {
BunkerRequestUtils.sendBunkerResponse(
context,
thisAccount,
request,
BunkerResponse(request.request.id, localEvent.toJson(), null),
application.application.relays,
onLoading = {},
onDone = {},
)
} else {
AmberUtils.sendBunkerError(
account = thisAccount,
bunkerRequest = request,
relays = application.application.relays,
context = context,
closeApplication = application.application.closeApplication,
onLoading = {},
)
}
} else if (request.request is BunkerRequestConnect) {
if (savedApplication == null) {
dao.insertApplicationWithPermissions(application) dao.insertApplicationWithPermissions(application)
historyDatabase.dao().addHistory( historyDatabase.dao().addHistory(
listOf( listOf(
HistoryEntity( HistoryEntity(
id = 0, 0,
pkKey = localKey, localKey,
type = SignerType.SIGN_EVENT.toString(), SignerType.CONNECT.toString(),
kind = localEvent.kind, null,
time = TimeUtils.now(), TimeUtils.now(),
accepted = isChecked, isApproved,
content = localEvent.toJson(), content = "",
), ),
), ),
thisAccount.npub, thisAccount.npub,
) )
BunkerRequestUtils.remove(request.request.id) BunkerRequestUtils.remove(request.request.id)
if (isApproved) {
if (isChecked) {
BunkerRequestUtils.sendBunkerResponse( BunkerRequestUtils.sendBunkerResponse(
context, context,
thisAccount, thisAccount,
request, request,
BunkerResponse(request.request.id, localEvent.toJson(), null), BunkerResponse(request.request.id, "", null),
application.application.relays, application.application.relays,
onLoading = {}, onLoading = {},
onDone = {}, onDone = {},
@@ -456,93 +354,54 @@ fun BunkerMultiEventHomeScreen(
onLoading = {}, onLoading = {},
) )
} }
} else if (request.request is BunkerRequestConnect) { }
if (savedApplication == null) { } else {
dao.insertApplicationWithPermissions(application) val type = requestType
if (rememberType != RememberType.NEVER) {
historyDatabase.dao().addHistory( val decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(type)
listOf( val permissionKind = if (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) {
HistoryEntity( if (decryptTypeScope == DecryptTypeScope.SPECIFIC) BunkerRequestUtils.getNip44v3Kind(request.request) else null
0, } else {
localKey, null
SignerType.CONNECT.toString(),
null,
TimeUtils.now(),
isChecked,
content = "",
),
),
thisAccount.npub,
)
BunkerRequestUtils.remove(request.request.id)
if (isChecked) {
BunkerRequestUtils.sendBunkerResponse(
context,
thisAccount,
request,
BunkerResponse(request.request.id, "", null),
application.application.relays,
onLoading = {},
onDone = {},
)
} else {
AmberUtils.sendBunkerError(
account = thisAccount,
bunkerRequest = request,
relays = application.application.relays,
context = context,
closeApplication = application.application.closeApplication,
onLoading = {},
)
}
} }
} else { AmberUtils.acceptOrRejectPermission(
val type = requestType application,
if (rememberType != RememberType.NEVER && isChecked) { localKey,
val decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(type) type,
val permissionKind = if (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) { permissionKind,
if (decryptTypeScope == DecryptTypeScope.SPECIFIC) BunkerRequestUtils.getNip44v3Kind(request.request) else null isApproved,
} else { rememberType,
null thisAccount,
} encryptedData = request.encryptedData,
AmberUtils.acceptOrRejectPermission( decryptTypeScope = decryptTypeScope,
application,
localKey,
type,
permissionKind,
true,
rememberType,
thisAccount,
encryptedData = request.encryptedData,
decryptTypeScope = decryptTypeScope,
)
}
dao.insertApplicationWithPermissions(application)
historyDatabase.dao().addHistory(
listOf(
HistoryEntity(
0,
localKey,
type.toString(),
null,
TimeUtils.now(),
isChecked,
content = if (type == SignerType.NIP04_DECRYPT || type == SignerType.NIP44_DECRYPT || type == SignerType.DECRYPT_ZAP_EVENT) {
request.encryptedData?.result ?: ""
} else {
request.request.params.getOrElse(1) { "" }
},
),
),
thisAccount.npub,
) )
}
val signature = request.encryptedData?.result ?: continue dao.insertApplicationWithPermissions(application)
BunkerRequestUtils.remove(request.request.id)
if (isChecked) { historyDatabase.dao().addHistory(
listOf(
HistoryEntity(
0,
localKey,
type.toString(),
null,
TimeUtils.now(),
isApproved,
content = if (type == SignerType.NIP04_DECRYPT || type == SignerType.NIP44_DECRYPT || type == SignerType.DECRYPT_ZAP_EVENT) {
request.encryptedData?.result ?: ""
} else {
request.request.params.getOrElse(1) { "" }
},
),
),
thisAccount.npub,
)
if (isApproved) {
val signature = request.encryptedData?.result
if (signature != null) {
BunkerRequestUtils.remove(request.request.id)
BunkerRequestUtils.sendBunkerResponse( BunkerRequestUtils.sendBunkerResponse(
context, context,
thisAccount, thisAccount,
@@ -552,25 +411,26 @@ fun BunkerMultiEventHomeScreen(
onLoading = {}, onLoading = {},
onDone = {}, onDone = {},
) )
} else {
AmberUtils.sendBunkerError(
account = thisAccount,
bunkerRequest = request,
relays = application.application.relays,
context = context,
closeApplication = application.application.closeApplication,
onLoading = {},
)
} }
} else {
BunkerRequestUtils.remove(request.request.id)
AmberUtils.sendBunkerError(
account = thisAccount,
bunkerRequest = request,
relays = application.application.relays,
context = context,
closeApplication = application.application.closeApplication,
onLoading = {},
)
} }
} }
} finally {
onLoading(false)
} }
} finally {
onLoading(false)
} }
}, }
) },
} )
} }
} }
@@ -578,8 +438,8 @@ fun BunkerMultiEventHomeScreen(
private fun BunkerRequestCard( private fun BunkerRequestCard(
context: Context, context: Context,
bunkerRequest: AmberBunkerRequest, bunkerRequest: AmberBunkerRequest,
checked: Boolean, approved: Boolean,
onToggleChecked: () -> Unit, onApproveChanged: (Boolean) -> Unit,
) { ) {
val type = BunkerRequestUtils.getTypeFromBunker(bunkerRequest.request) val type = BunkerRequestUtils.getTypeFromBunker(bunkerRequest.request)
var showDetails by remember { mutableStateOf(false) } var showDetails by remember { mutableStateOf(false) }
@@ -659,42 +519,49 @@ private fun BunkerRequestCard(
), ),
border = BorderStroke(1.dp, Color.Gray), border = BorderStroke(1.dp, Color.Gray),
) { ) {
Row( Column(
verticalAlignment = Alignment.CenterVertically, Modifier.padding(8.dp),
modifier = Modifier
.fillMaxWidth()
.clickable { onToggleChecked() },
) { ) {
Checkbox( Row(
checked = checked, verticalAlignment = Alignment.CenterVertically,
onCheckedChange = { onToggleChecked() }, modifier = Modifier.fillMaxWidth(),
colors = CheckboxDefaults.colors().copy(
uncheckedBorderColor = Color.Gray,
),
)
Column(
Modifier
.weight(1f)
.padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
) { ) {
Text( Column(
text = label, Modifier
color = if (checked) Color.Unspecified else Color.Gray, .weight(1f)
) .padding(end = 8.dp),
if (preview.isNotBlank()) { ) {
Text( Text(text = label)
text = preview, if (preview.isNotBlank()) {
color = Color.Gray, Text(
maxLines = 2, text = preview,
) color = Color.Gray,
} maxLines = 2,
if (hasDetails) { )
RawJsonButton( }
onCLick = { showDetails = true }, if (hasDetails) {
text = stringResource(R.string.show_details), RawJsonButton(
) onCLick = { showDetails = true },
text = stringResource(R.string.show_details),
)
}
} }
} }
AmberToggles(
selected = approved,
options = listOf(true, false),
onSelected = onApproveChanged,
label = {
if (it) stringResource(R.string.approve) else stringResource(R.string.deny)
},
indicatorColor = {
if (it) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
},
selectedTextColor = {
if (it) Color.Black else MaterialTheme.colorScheme.onError
},
)
} }
} }
@@ -4,22 +4,16 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -32,7 +26,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.text.toLowerCase
@@ -58,7 +51,6 @@ import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents
import com.greenart7c3.nostrsigner.service.RelayUrlUtils import com.greenart7c3.nostrsigner.service.RelayUrlUtils
import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.model.AmberEvent
import com.greenart7c3.nostrsigner.ui.RememberType import com.greenart7c3.nostrsigner.ui.RememberType
import com.greenart7c3.nostrsigner.ui.theme.orange
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -82,6 +74,7 @@ fun IntentMultiEventHomeScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
MultiEventScreenIntents.checkedStates.clear() MultiEventScreenIntents.checkedStates.clear()
MultiEventScreenIntents.rememberType = RememberType.NEVER MultiEventScreenIntents.rememberType = RememberType.NEVER
// checkedStates now holds the per-request decision: true = Approve, false = Deny.
intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = true } intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = true }
} }
@@ -99,30 +92,6 @@ fun IntentMultiEventHomeScreen(
SigningAs(accountParam) SigningAs(accountParam)
val allCheckedState = when {
intents.all { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.On
intents.none { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.Off
else -> ToggleableState.Indeterminate
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
val newValue = allCheckedState != ToggleableState.On
MultiEventScreenIntents.checkedStates.putAll(intents.associate { it.id to newValue })
},
) {
TriStateCheckbox(
state = allCheckedState,
onClick = {
val newValue = allCheckedState != ToggleableState.On
MultiEventScreenIntents.checkedStates.putAll(intents.associate { it.id to newValue })
},
)
Text(stringResource(R.string.select_deselect_all))
}
val groups = remember(intents) { val groups = remember(intents) {
groupRequests(intents) { groupRequests(intents) {
requestGroupKey(it.type, it.event?.kind, it.encryptedData, it.nip44v3Kind) requestGroupKey(it.type, it.event?.kind, it.encryptedData, it.nip44v3Kind)
@@ -136,19 +105,14 @@ fun IntentMultiEventHomeScreen(
val expanded = groups.size == 1 || (expandedGroups[groupKey] ?: false) val expanded = groups.size == 1 || (expandedGroups[groupKey] ?: false)
if (groups.size > 1) { if (groups.size > 1) {
item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") {
val groupState = when { val groupApproved = groupIntents.all { MultiEventScreenIntents.checkedStates[it.id] ?: true }
groupIntents.all { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.On
groupIntents.none { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.Off
else -> ToggleableState.Indeterminate
}
RequestGroupHeader( RequestGroupHeader(
label = groupKey.toLabel(context), label = groupKey.toLabel(context),
count = groupIntents.size, count = groupIntents.size,
state = groupState, approved = groupApproved,
expanded = expanded, expanded = expanded,
onToggle = { onApproveChanged = { approve ->
val newValue = groupState != ToggleableState.On MultiEventScreenIntents.checkedStates.putAll(groupIntents.associate { it.id to approve })
MultiEventScreenIntents.checkedStates.putAll(groupIntents.associate { it.id to newValue })
}, },
onExpandToggle = { onExpandToggle = {
expandedGroups[groupKey] = !(expandedGroups[groupKey] ?: false) expandedGroups[groupKey] = !(expandedGroups[groupKey] ?: false)
@@ -174,10 +138,9 @@ fun IntentMultiEventHomeScreen(
IntentRequestCard( IntentRequestCard(
context = context, context = context,
intent = intent, intent = intent,
checked = MultiEventScreenIntents.checkedStates[intent.id] ?: true, approved = MultiEventScreenIntents.checkedStates[intent.id] ?: true,
onToggleChecked = { onApproveChanged = {
val current = MultiEventScreenIntents.checkedStates[intent.id] ?: true MultiEventScreenIntents.checkedStates[intent.id] = it
MultiEventScreenIntents.checkedStates[intent.id] = !current
}, },
) )
} }
@@ -185,19 +148,16 @@ fun IntentMultiEventHomeScreen(
} }
} }
Row( AmberButton(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 16.dp), .padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), text = stringResource(R.string.confirm),
) { onClick = {
AmberButton( onLoading(true)
Modifier.weight(1f), Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
colors = ButtonDefaults.buttonColors().copy( try {
containerColor = orange, val results = mutableListOf<Result>()
),
onClick = {
Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
var closeApp = true var closeApp = true
onRemoveIntentData(intents, IntentResultType.REMOVE) onRemoveIntentData(intents, IntentResultType.REMOVE)
val localKey = packageName ?: return@launch val localKey = packageName ?: return@launch
@@ -211,6 +171,7 @@ fun IntentMultiEventHomeScreen(
} ?: continue } ?: continue
val dao = Amber.instance.dao(thisAccount.npub) val dao = Amber.instance.dao(thisAccount.npub)
val historyDatabase = Amber.instance.getHistoryDatabase(thisAccount.npub)
val application = dao.getByKey(localKey) ?: ApplicationWithPermissions( val application = dao.getByKey(localKey) ?: ApplicationWithPermissions(
application = ApplicationEntity( application = ApplicationEntity(
localKey, localKey,
@@ -236,247 +197,163 @@ fun IntentMultiEventHomeScreen(
} }
var permissionsChanged = false var permissionsChanged = false
val historyList = mutableListOf<HistoryEntity>()
for (intentData in accountIntents) { for (intentData in accountIntents) {
val isChecked = MultiEventScreenIntents.checkedStates[intentData.id] ?: true val isApproved = MultiEventScreenIntents.checkedStates[intentData.id] ?: true
val groupKey = requestGroupKey(intentData.type, intentData.event?.kind, intentData.encryptedData, intentData.nip44v3Kind) val type = intentData.type
val groupKey = requestGroupKey(type, intentData.event?.kind, intentData.encryptedData, intentData.nip44v3Kind)
val rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER val rememberType = groupRememberTypes[groupKey] ?: RememberType.NEVER
if (rememberType != RememberType.NEVER && isChecked) {
val decryptTypeScope = groupDecryptScopes[groupKey] ?: defaultDecryptTypeScope(intentData.type) if (type == SignerType.SIGN_EVENT) {
val rejectKind = when { val localEvent = intentData.event!!
intentData.type == SignerType.SIGN_EVENT -> intentData.event?.kind
intentData.type == SignerType.NIP44_V3_ENCRYPT || intentData.type == SignerType.NIP44_V3_DECRYPT -> if (rememberType != RememberType.NEVER) {
if (decryptTypeScope == DecryptTypeScope.SPECIFIC) intentData.nip44v3Kind else null val signRelay = if (localEvent.kind == 22242) {
else -> null if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) {
} "*"
val rejectRelay = if (intentData.type == SignerType.SIGN_EVENT && intentData.event?.kind == 22242) { } else {
if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) { RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent))
"*" }
} else { } else {
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(intentData.event)) ""
}
AmberUtils.updatePermission(
application,
localKey,
type,
localEvent.kind,
isApproved,
rememberType,
relay = signRelay,
encryptedData = intentData.encryptedData,
)
permissionsChanged = true
}
historyList.add(
HistoryEntity(
0,
localKey,
type.toString(),
localEvent.kind,
TimeUtils.now(),
isApproved,
content = localEvent.toJson(),
),
)
if (isApproved) {
val signature = if (localEvent is LnZapRequestEvent &&
localEvent.tags.any { tag ->
tag.any { t -> t == "anon" }
}
) {
localEvent.toJson()
} else {
localEvent.sig
}
results.add(
Result(
null,
signature = signature,
result = signature,
id = intentData.id,
rejected = null,
),
)
} else {
results.add(
Result(
null,
signature = null,
result = null,
id = intentData.id,
rejected = true,
),
)
}
} else {
if (rememberType != RememberType.NEVER) {
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,
permissionKind,
isApproved,
rememberType,
encryptedData = intentData.encryptedData,
decryptTypeScope = decryptTypeScope,
)
permissionsChanged = true
}
historyList.add(
HistoryEntity(
0,
localKey,
type.toString(),
null,
TimeUtils.now(),
isApproved,
content = if (type == SignerType.NIP04_DECRYPT || type == SignerType.NIP44_DECRYPT || type == SignerType.DECRYPT_ZAP_EVENT) {
intentData.encryptedData?.result ?: ""
} else {
intentData.data
},
),
)
if (isApproved) {
val signature = intentData.encryptedData?.result
if (signature != null) {
results.add(
Result(
null,
signature = signature,
result = signature,
id = intentData.id,
rejected = null,
),
)
} }
} else { } else {
"" results.add(
Result(
null,
signature = null,
result = null,
id = intentData.id,
rejected = true,
),
)
} }
AmberUtils.updatePermission(
application,
localKey,
intentData.type,
rejectKind,
false,
rememberType,
relay = rejectRelay,
encryptedData = intentData.encryptedData,
decryptTypeScope = decryptTypeScope,
)
permissionsChanged = true
} }
} }
if (permissionsChanged || application.application.key.isBlank()) { if (permissionsChanged || application.application.key.isBlank()) {
dao.insertApplicationWithPermissions(application) dao.insertApplicationWithPermissions(application)
} }
historyDatabase.dao().addHistory(historyList, thisAccount.npub)
}
if (results.isNotEmpty()) {
sendResultIntent(results)
} }
sendRejectIntent(
results = intents.map {
Result(
null,
signature = null,
result = null,
id = it.id,
rejected = true,
)
}.toMutableList(),
)
finishActivity(closeApp) finishActivity(closeApp)
} finally {
onLoading(false)
} }
}, }
text = stringResource(R.string.discard_all), },
) )
AmberButton(
Modifier.weight(1f),
text = stringResource(R.string.approve_all),
onClick = {
onLoading(true)
Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
try {
val results = mutableListOf<Result>()
var closeApp = true
onRemoveIntentData(intents, IntentResultType.REMOVE)
val localKey = packageName ?: return@launch
val intentsByAccount = intents.groupBy { it.currentAccount.ifBlank { accountParam.npub } }
for ((accountNpub, accountIntents) in intentsByAccount) {
val thisAccount = if (accountNpub == accountParam.npub) {
accountParam
} else {
LocalPreferences.loadFromEncryptedStorage(context, accountNpub)
} ?: continue
val dao = Amber.instance.dao(thisAccount.npub)
val historyDatabase = Amber.instance.getHistoryDatabase(thisAccount.npub)
val application = dao.getByKey(localKey) ?: ApplicationWithPermissions(
application = ApplicationEntity(
localKey,
"",
listOf(),
"",
"",
"",
thisAccount.hexKey,
true,
"",
false,
thisAccount.signPolicy,
true,
0L,
lastUsed = TimeUtils.now(),
),
permissions = mutableListOf(),
)
if (!application.application.closeApplication) {
closeApp = false
}
var permissionsChanged = false
val historyList = mutableListOf<HistoryEntity>()
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 ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) {
"*"
} else {
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent))
}
} else {
""
}
AmberUtils.updatePermission(
application,
localKey,
type,
localEvent.kind,
true,
rememberType,
relay = signRelay,
encryptedData = intentData.encryptedData,
)
permissionsChanged = true
}
historyList.add(
HistoryEntity(
0,
localKey,
type.toString(),
localEvent.kind,
TimeUtils.now(),
isChecked,
content = localEvent.toJson(),
),
)
if (isChecked) {
val signature = if (localEvent is LnZapRequestEvent &&
localEvent.tags.any { tag ->
tag.any { t -> t == "anon" }
}
) {
localEvent.toJson()
} else {
localEvent.sig
}
results.add(
Result(
null,
signature = signature,
result = signature,
id = intentData.id,
rejected = null,
),
)
}
} 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,
permissionKind,
true,
rememberType,
encryptedData = intentData.encryptedData,
decryptTypeScope = decryptTypeScope,
)
permissionsChanged = true
}
historyList.add(
HistoryEntity(
0,
localKey,
type.toString(),
null,
TimeUtils.now(),
isChecked,
content = if (type == SignerType.NIP04_DECRYPT || type == SignerType.NIP44_DECRYPT || type == SignerType.DECRYPT_ZAP_EVENT) {
intentData.encryptedData?.result ?: ""
} else {
intentData.data
},
),
)
val signature = intentData.encryptedData?.result
if (isChecked && signature != null) {
results.add(
Result(
null,
signature = signature,
result = signature,
id = intentData.id,
rejected = null,
),
)
}
}
}
if (permissionsChanged || application.application.key.isBlank()) {
dao.insertApplicationWithPermissions(application)
}
historyDatabase.dao().addHistory(historyList, thisAccount.npub)
}
if (results.isNotEmpty()) {
sendResultIntent(results)
}
finishActivity(closeApp)
} finally {
onLoading(false)
}
}
},
)
}
} }
} }
@@ -484,8 +361,8 @@ fun IntentMultiEventHomeScreen(
private fun IntentRequestCard( private fun IntentRequestCard(
context: Context, context: Context,
intent: IntentData, intent: IntentData,
checked: Boolean, approved: Boolean,
onToggleChecked: () -> Unit, onApproveChanged: (Boolean) -> Unit,
) { ) {
val type = intent.type val type = intent.type
var showDetails by remember { mutableStateOf(false) } var showDetails by remember { mutableStateOf(false) }
@@ -569,42 +446,49 @@ private fun IntentRequestCard(
), ),
border = BorderStroke(1.dp, Color.Gray), border = BorderStroke(1.dp, Color.Gray),
) { ) {
Row( Column(
verticalAlignment = Alignment.CenterVertically, Modifier.padding(8.dp),
modifier = Modifier
.fillMaxWidth()
.clickable { onToggleChecked() },
) { ) {
Checkbox( Row(
checked = checked, verticalAlignment = Alignment.CenterVertically,
onCheckedChange = { onToggleChecked() }, modifier = Modifier.fillMaxWidth(),
colors = CheckboxDefaults.colors().copy(
uncheckedBorderColor = Color.Gray,
),
)
Column(
Modifier
.weight(1f)
.padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
) { ) {
Text( Column(
text = label, Modifier
color = if (checked) Color.Unspecified else Color.Gray, .weight(1f)
) .padding(end = 8.dp),
if (preview.isNotBlank()) { ) {
Text( Text(text = label)
text = preview, if (preview.isNotBlank()) {
color = Color.Gray, Text(
maxLines = 2, text = preview,
) color = Color.Gray,
} maxLines = 2,
if (hasDetails) { )
RawJsonButton( }
onCLick = { showDetails = true }, if (hasDetails) {
text = stringResource(R.string.show_details), RawJsonButton(
) onCLick = { showDetails = true },
text = stringResource(R.string.show_details),
)
}
} }
} }
AmberToggles(
selected = approved,
options = listOf(true, false),
onSelected = onApproveChanged,
label = {
if (it) stringResource(R.string.approve) else stringResource(R.string.deny)
},
indicatorColor = {
if (it) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
},
selectedTextColor = {
if (it) Color.Black else MaterialTheme.colorScheme.onError
},
)
} }
} }
@@ -632,15 +516,6 @@ private fun finishActivity(closeApp: Boolean) {
} }
} }
private fun sendRejectIntent(
results: MutableList<Result>,
) {
val json = Permission.mapper.writeValueAsString(results)
val intent = Intent()
intent.putExtra("results", json)
Amber.instance.getMainActivity()?.setResult(Activity.RESULT_OK, intent)
}
private fun sendResultIntent( private fun sendResultIntent(
results: MutableList<Result>, results: MutableList<Result>,
) { ) {
@@ -63,9 +63,10 @@ fun LabeledBorderBox(
fun RememberMyChoiceToggles( fun RememberMyChoiceToggles(
selected: RememberType, selected: RememberType,
onSelected: (RememberType) -> Unit, onSelected: (RememberType) -> Unit,
label: String = stringResource(R.string.automatically_sign_this_for),
) { ) {
LabeledBorderBox( LabeledBorderBox(
label = stringResource(R.string.automatically_sign_this_for), label = label,
) { ) {
AmberToggles( AmberToggles(
selected = selected, selected = selected,
@@ -12,14 +12,13 @@ import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind
@@ -226,6 +225,7 @@ fun RequestGroupOptions(
RememberMyChoiceToggles( RememberMyChoiceToggles(
selected = rememberType, selected = rememberType,
onSelected = onRememberTypeChanged, onSelected = onRememberTypeChanged,
label = stringResource(R.string.remember_my_choice_for),
) )
} }
} }
@@ -234,36 +234,51 @@ fun RequestGroupOptions(
fun RequestGroupHeader( fun RequestGroupHeader(
label: String, label: String,
count: Int, count: Int,
state: ToggleableState, approved: Boolean,
expanded: Boolean, expanded: Boolean,
onToggle: () -> Unit, onApproveChanged: (Boolean) -> Unit,
onExpandToggle: () -> Unit, onExpandToggle: () -> Unit,
) { ) {
val rotation by animateFloatAsState( val rotation by animateFloatAsState(
targetValue = if (expanded) 180f else 0f, targetValue = if (expanded) 180f else 0f,
label = "group header chevron rotation", label = "group header chevron rotation",
) )
Row( Column(
verticalAlignment = Alignment.CenterVertically, Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.clickable { onExpandToggle() },
) { ) {
TriStateCheckbox( Row(
state = state, verticalAlignment = Alignment.CenterVertically,
onClick = onToggle,
)
Text(
text = "$label ($count)",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.weight(1f),
)
Icon(
imageVector = Icons.Default.ExpandMore,
contentDescription = null,
modifier = Modifier modifier = Modifier
.padding(end = 12.dp) .fillMaxWidth()
.rotate(rotation), .clickable { onExpandToggle() },
) {
Text(
text = "$label ($count)",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.weight(1f),
)
Icon(
imageVector = Icons.Default.ExpandMore,
contentDescription = null,
modifier = Modifier
.padding(end = 12.dp)
.rotate(rotation),
)
}
AmberToggles(
selected = approved,
options = listOf(true, false),
onSelected = onApproveChanged,
label = {
if (it) stringResource(R.string.approve) else stringResource(R.string.deny)
},
indicatorColor = {
if (it) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
},
selectedTextColor = {
if (it) Color.Black else MaterialTheme.colorScheme.onError
},
) )
} }
} }
@@ -25,11 +25,12 @@ import com.greenart7c3.nostrsigner.ui.theme.ThemePreviews
fun ToggleOption( fun ToggleOption(
text: String, text: String,
isSelected: Boolean, isSelected: Boolean,
selectedTextColor: Color,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val textColor by animateColorAsState( val textColor by animateColorAsState(
targetValue = if (isSelected) Color.Black else MaterialTheme.colorScheme.onSurfaceVariant, targetValue = if (isSelected) selectedTextColor else MaterialTheme.colorScheme.onSurfaceVariant,
animationSpec = tween(durationMillis = 200), animationSpec = tween(durationMillis = 200),
label = "textColor", label = "textColor",
) )
@@ -59,12 +60,14 @@ fun ToggleOptionPreview() {
ToggleOption( ToggleOption(
text = "Always", text = "Always",
isSelected = true, isSelected = true,
selectedTextColor = Color.Black,
modifier = Modifier.width(80.dp), modifier = Modifier.width(80.dp),
onClick = {}, onClick = {},
) )
ToggleOption( ToggleOption(
text = "Never", text = "Never",
isSelected = false, isSelected = false,
selectedTextColor = Color.Black,
modifier = Modifier.width(80.dp), modifier = Modifier.width(80.dp),
onClick = {}, onClick = {},
) )
+3 -2
View File
@@ -630,8 +630,9 @@
<string name="pubkey">Pubkey</string> <string name="pubkey">Pubkey</string>
<string name="date">Datum</string> <string name="date">Datum</string>
<string name="tags">Tags</string> <string name="tags">Tags</string>
<string name="discard_all">Auswahl verwerfen</string> <string name="approve">Genehmigen</string>
<string name="approve_all">Auswahl genehmigen</string> <string name="confirm">Bestätigen</string>
<string name="remember_my_choice_for">Meine Wahl speichern für</string>
<string name="requests_message">Anfragen %1$s</string> <string name="requests_message">Anfragen %1$s</string>
<string name="trust_excellent">Ausgezeichnet</string> <string name="trust_excellent">Ausgezeichnet</string>
<string name="trust_good">Gut</string> <string name="trust_good">Gut</string>
+3 -2
View File
@@ -633,8 +633,9 @@
<string name="pubkey">Clave pública</string> <string name="pubkey">Clave pública</string>
<string name="date">Fecha</string> <string name="date">Fecha</string>
<string name="tags">Etiquetas</string> <string name="tags">Etiquetas</string>
<string name="discard_all">Descartar seleccionados</string> <string name="approve">Aprobar</string>
<string name="approve_all">Aprobar seleccionados</string> <string name="confirm">Confirmar</string>
<string name="remember_my_choice_for">Recordar mi elección para</string>
<string name="requests_message">Solicitudes %1$s</string> <string name="requests_message">Solicitudes %1$s</string>
<string name="trust_excellent">Excelente</string> <string name="trust_excellent">Excelente</string>
<string name="trust_good">Bueno</string> <string name="trust_good">Bueno</string>
+3 -2
View File
@@ -630,8 +630,9 @@
<string name="pubkey">Pubkey</string> <string name="pubkey">Pubkey</string>
<string name="date">Date</string> <string name="date">Date</string>
<string name="tags">Tags</string> <string name="tags">Tags</string>
<string name="discard_all">Rejeter la sélection</string> <string name="approve">Approuver</string>
<string name="approve_all">Approuver la sélection</string> <string name="confirm">Confirmer</string>
<string name="remember_my_choice_for">Mémoriser mon choix pour</string>
<string name="requests_message">Requêtes %1$s</string> <string name="requests_message">Requêtes %1$s</string>
<string name="trust_excellent">Excellent</string> <string name="trust_excellent">Excellent</string>
<string name="trust_good">Bon</string> <string name="trust_good">Bon</string>
+3 -2
View File
@@ -633,8 +633,9 @@
<string name="pubkey">Pubkey</string> <string name="pubkey">Pubkey</string>
<string name="date">Tanggal</string> <string name="date">Tanggal</string>
<string name="tags">Tag</string> <string name="tags">Tag</string>
<string name="discard_all">Buang yang dipilih</string> <string name="approve">Setujui</string>
<string name="approve_all">Setujui yang dipilih</string> <string name="confirm">Konfirmasi</string>
<string name="remember_my_choice_for">Ingat pilihan saya untuk</string>
<string name="requests_message">Permintaan %1$s</string> <string name="requests_message">Permintaan %1$s</string>
<string name="trust_excellent">Sangat baik</string> <string name="trust_excellent">Sangat baik</string>
<string name="trust_good">Baik</string> <string name="trust_good">Baik</string>
+3 -2
View File
@@ -633,8 +633,9 @@
<string name="pubkey">Chiave pubblica</string> <string name="pubkey">Chiave pubblica</string>
<string name="date">Data</string> <string name="date">Data</string>
<string name="tags">Tag</string> <string name="tags">Tag</string>
<string name="discard_all">Scarta selezionati</string> <string name="approve">Approva</string>
<string name="approve_all">Approva selezionati</string> <string name="confirm">Conferma</string>
<string name="remember_my_choice_for">Ricorda la mia scelta per</string>
<string name="requests_message">Richieste %1$s</string> <string name="requests_message">Richieste %1$s</string>
<string name="trust_excellent">Eccellente</string> <string name="trust_excellent">Eccellente</string>
<string name="trust_good">Buono</string> <string name="trust_good">Buono</string>
+3 -2
View File
@@ -609,8 +609,9 @@
<string name="pubkey">Pubkey</string> <string name="pubkey">Pubkey</string>
<string name="date">日付</string> <string name="date">日付</string>
<string name="tags">タグ</string> <string name="tags">タグ</string>
<string name="discard_all">選択した項目を破棄</string> <string name="approve">承認</string>
<string name="approve_all">選択した項目を承認</string> <string name="confirm">確認</string>
<string name="remember_my_choice_for">この選択を記憶する対象</string>
<string name="requests_message">リクエスト %1$s</string> <string name="requests_message">リクエスト %1$s</string>
<string name="trust_excellent">非常に高い</string> <string name="trust_excellent">非常に高い</string>
<string name="trust_good">高い</string> <string name="trust_good">高い</string>
+3 -2
View File
@@ -633,8 +633,9 @@
<string name="pubkey">공개 키</string> <string name="pubkey">공개 키</string>
<string name="date">날짜</string> <string name="date">날짜</string>
<string name="tags">태그</string> <string name="tags">태그</string>
<string name="discard_all">선택 항목 버리기</string> <string name="approve">승인</string>
<string name="approve_all">선택 항목 승인</string> <string name="confirm">확인</string>
<string name="remember_my_choice_for">내 선택 기억</string>
<string name="requests_message">요청 %1$s</string> <string name="requests_message">요청 %1$s</string>
<string name="trust_excellent">매우 좋음</string> <string name="trust_excellent">매우 좋음</string>
<string name="trust_good">좋음</string> <string name="trust_good">좋음</string>
+3 -2
View File
@@ -628,8 +628,9 @@
<string name="pubkey">Chave</string> <string name="pubkey">Chave</string>
<string name="date">Data</string> <string name="date">Data</string>
<string name="tags">Tags</string> <string name="tags">Tags</string>
<string name="discard_all">Descartar selecionado</string> <string name="approve">Aprovar</string>
<string name="approve_all">Aprovar selecionados</string> <string name="confirm">Confirmar</string>
<string name="remember_my_choice_for">Lembrar minha escolha para</string>
<string name="requests_message">Solicitações %1$s</string> <string name="requests_message">Solicitações %1$s</string>
<string name="trust_excellent">Excelente</string> <string name="trust_excellent">Excelente</string>
<string name="trust_good">Bom</string> <string name="trust_good">Bom</string>
+3 -2
View File
@@ -633,8 +633,9 @@
<string name="pubkey">Публичный ключ</string> <string name="pubkey">Публичный ключ</string>
<string name="date">Дата</string> <string name="date">Дата</string>
<string name="tags">Теги</string> <string name="tags">Теги</string>
<string name="discard_all">Отклонить выбранные</string> <string name="approve">Одобрить</string>
<string name="approve_all">Одобрить выбранные</string> <string name="confirm">Подтвердить</string>
<string name="remember_my_choice_for">Запомнить мой выбор для</string>
<string name="requests_message">Запросы %1$s</string> <string name="requests_message">Запросы %1$s</string>
<string name="trust_excellent">Отлично</string> <string name="trust_excellent">Отлично</string>
<string name="trust_good">Хорошо</string> <string name="trust_good">Хорошо</string>
+3 -2
View File
@@ -609,8 +609,9 @@
<string name="pubkey">Pubkey</string> <string name="pubkey">Pubkey</string>
<string name="date">วันที่</string> <string name="date">วันที่</string>
<string name="tags">แท็ก</string> <string name="tags">แท็ก</string>
<string name="discard_all">ยกเลิกรายการที่เลือก</string> <string name="approve">อนุมัติ</string>
<string name="approve_all">อนุมัติรายการที่เลือก</string> <string name="confirm">ยืนยัน</string>
<string name="remember_my_choice_for">จดจำตัวเลือกของฉันสำหรับ</string>
<string name="requests_message">คำขอ %1$s</string> <string name="requests_message">คำขอ %1$s</string>
<string name="trust_excellent">ยอดเยี่ยม</string> <string name="trust_excellent">ยอดเยี่ยม</string>
<string name="trust_good">ดี</string> <string name="trust_good">ดี</string>
+3 -2
View File
@@ -629,8 +629,9 @@
<string name="pubkey">Pubkey (Açık anahtar)</string> <string name="pubkey">Pubkey (Açık anahtar)</string>
<string name="date">Tarih</string> <string name="date">Tarih</string>
<string name="tags">Etiketler</string> <string name="tags">Etiketler</string>
<string name="discard_all">Seçilenleri yoksay</string> <string name="approve">Onayla</string>
<string name="approve_all">Seçilenleri onayla</string> <string name="confirm">Doğrula</string>
<string name="remember_my_choice_for">Seçimimi şu için hatırla</string>
<string name="requests_message">İstekler: %1$s</string> <string name="requests_message">İstekler: %1$s</string>
<string name="trust_excellent">Mükemmel</string> <string name="trust_excellent">Mükemmel</string>
<string name="trust_good">İyi</string> <string name="trust_good">İyi</string>
+3 -2
View File
@@ -609,8 +609,9 @@
<string name="pubkey">Pubkey</string> <string name="pubkey">Pubkey</string>
<string name="date">Ngày</string> <string name="date">Ngày</string>
<string name="tags">Thẻ</string> <string name="tags">Thẻ</string>
<string name="discard_all">Bỏ qua đã chọn</string> <string name="approve">Phê duyệt</string>
<string name="approve_all">Phê duyệt đã chọn</string> <string name="confirm">Xác nhận</string>
<string name="remember_my_choice_for">Ghi nhớ lựa chọn của tôi cho</string>
<string name="requests_message">Yêu cầu %1$s</string> <string name="requests_message">Yêu cầu %1$s</string>
<string name="trust_excellent">Xuất sắc</string> <string name="trust_excellent">Xuất sắc</string>
<string name="trust_good">Tốt</string> <string name="trust_good">Tốt</string>
+3 -2
View File
@@ -614,8 +614,9 @@
<string name="pubkey">公钥</string> <string name="pubkey">公钥</string>
<string name="date">日期</string> <string name="date">日期</string>
<string name="tags">标签</string> <string name="tags">标签</string>
<string name="discard_all">拒绝所选</string> <string name="approve">批准</string>
<string name="approve_all">批准所选</string> <string name="confirm">确认</string>
<string name="remember_my_choice_for">为此记住我的选择</string>
<string name="requests_message">请求 %1$s</string> <string name="requests_message">请求 %1$s</string>
<string name="trust_excellent">极佳</string> <string name="trust_excellent">极佳</string>
<string name="trust_good">良好</string> <string name="trust_good">良好</string>
+3 -2
View File
@@ -661,8 +661,9 @@
<string name="pubkey">Pubkey</string> <string name="pubkey">Pubkey</string>
<string name="date">Date</string> <string name="date">Date</string>
<string name="tags">Tags</string> <string name="tags">Tags</string>
<string name="discard_all">Discard selected</string> <string name="approve">Approve</string>
<string name="approve_all">Approve selected</string> <string name="confirm">Confirm</string>
<string name="remember_my_choice_for">Remember my choice for</string>
<string name="requests_message">Requests %1$s</string> <string name="requests_message">Requests %1$s</string>
<string name="trust_excellent">Excellent</string> <string name="trust_excellent">Excellent</string>
<string name="trust_good">Good</string> <string name="trust_good">Good</string>