Redesign multi-request approval screen with Approve/Deny toggles (#497)
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:
@@ -23,6 +23,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -47,6 +48,8 @@ fun <T> AmberToggles(
|
||||
onSelected: (T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
label: @Composable (T) -> String,
|
||||
indicatorColor: @Composable (T) -> Color = { MaterialTheme.colorScheme.primary },
|
||||
selectedTextColor: @Composable (T) -> Color = { Color.Black },
|
||||
) {
|
||||
val count = options.size.coerceAtLeast(1)
|
||||
val selectedIndex = options.indexOf(selected).coerceAtLeast(0)
|
||||
@@ -100,7 +103,7 @@ fun <T> AmberToggles(
|
||||
.width(segmentWidth)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.primary),
|
||||
.background(indicatorColor(selected)),
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxHeight()) {
|
||||
@@ -109,6 +112,7 @@ fun <T> AmberToggles(
|
||||
modifier = Modifier.width(segmentWidth),
|
||||
text = label(option),
|
||||
isSelected = option == selected,
|
||||
selectedTextColor = selectedTextColor(option),
|
||||
onClick = { onSelected(option) },
|
||||
)
|
||||
}
|
||||
|
||||
+199
-332
@@ -2,22 +2,16 @@ package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import android.content.Context
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TriStateCheckbox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -30,7 +24,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
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.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.RememberType
|
||||
import com.greenart7c3.nostrsigner.ui.theme.orange
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
@@ -89,6 +81,7 @@ fun BunkerMultiEventHomeScreen(
|
||||
LaunchedEffect(Unit) {
|
||||
MultiEventScreenIntents.checkedStates.clear()
|
||||
MultiEventScreenIntents.rememberType = RememberType.NEVER
|
||||
// checkedStates now holds the per-request decision: true = Approve, false = Deny.
|
||||
bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = true }
|
||||
}
|
||||
|
||||
@@ -128,30 +121,6 @@ fun BunkerMultiEventHomeScreen(
|
||||
|
||||
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) {
|
||||
groupRequests(bunkerRequests) {
|
||||
requestGroupKey(
|
||||
@@ -170,19 +139,14 @@ fun BunkerMultiEventHomeScreen(
|
||||
val expanded = groups.size == 1 || (expandedGroups[groupKey] ?: false)
|
||||
if (groups.size > 1) {
|
||||
item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") {
|
||||
val groupState = when {
|
||||
groupItems.all { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.On
|
||||
groupItems.none { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.Off
|
||||
else -> ToggleableState.Indeterminate
|
||||
}
|
||||
val groupApproved = groupItems.all { MultiEventScreenIntents.checkedStates[it.request.id] ?: true }
|
||||
RequestGroupHeader(
|
||||
label = groupKey.toLabel(context),
|
||||
count = groupItems.size,
|
||||
state = groupState,
|
||||
approved = groupApproved,
|
||||
expanded = expanded,
|
||||
onToggle = {
|
||||
val newValue = groupState != ToggleableState.On
|
||||
MultiEventScreenIntents.checkedStates.putAll(groupItems.associate { it.request.id to newValue })
|
||||
onApproveChanged = { approve ->
|
||||
MultiEventScreenIntents.checkedStates.putAll(groupItems.associate { it.request.id to approve })
|
||||
},
|
||||
onExpandToggle = {
|
||||
expandedGroups[groupKey] = !(expandedGroups[groupKey] ?: false)
|
||||
@@ -208,10 +172,9 @@ fun BunkerMultiEventHomeScreen(
|
||||
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
|
||||
approved = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true,
|
||||
onApproveChanged = {
|
||||
MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] = it
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -219,21 +182,20 @@ fun BunkerMultiEventHomeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
AmberButton(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
AmberButton(
|
||||
Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors().copy(
|
||||
containerColor = orange,
|
||||
),
|
||||
onClick = {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
var closeApp = true
|
||||
text = stringResource(R.string.confirm),
|
||||
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()) {
|
||||
@@ -247,20 +209,23 @@ fun BunkerMultiEventHomeScreen(
|
||||
|
||||
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 =
|
||||
dao
|
||||
.getByKey(localKey) ?: ApplicationWithPermissions(
|
||||
savedApplication ?: ApplicationWithPermissions(
|
||||
application = ApplicationEntity(
|
||||
localKey,
|
||||
"",
|
||||
request.clientMetadata?.name ?: "",
|
||||
listOf(),
|
||||
"",
|
||||
"",
|
||||
request.clientMetadata?.url ?: "",
|
||||
request.clientMetadata?.image ?: "",
|
||||
"",
|
||||
thisAccount.hexKey,
|
||||
true,
|
||||
@@ -274,7 +239,7 @@ fun BunkerMultiEventHomeScreen(
|
||||
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 groupKey = requestGroupKey(
|
||||
type = requestType,
|
||||
@@ -283,165 +248,98 @@ fun BunkerMultiEventHomeScreen(
|
||||
nip44v3Kind = BunkerRequestUtils.getNip44v3Kind(request.request),
|
||||
)
|
||||
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) {
|
||||
closeApp = false
|
||||
}
|
||||
}
|
||||
if (request.request is BunkerRequestSign) {
|
||||
val localEvent = request.signedEvent!!
|
||||
|
||||
EventNotificationConsumer(context).notificationManager().cancelAll()
|
||||
finishActivity(closeApp)
|
||||
}
|
||||
},
|
||||
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))
|
||||
}
|
||||
if (rememberType != RememberType.NEVER) {
|
||||
val signRelay = if (localEvent.kind == 22242) {
|
||||
if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) {
|
||||
"*"
|
||||
} else {
|
||||
""
|
||||
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent))
|
||||
}
|
||||
AmberUtils.acceptOrRejectPermission(
|
||||
application = application,
|
||||
key = localKey,
|
||||
signerType = SignerType.SIGN_EVENT,
|
||||
kind = localEvent.kind,
|
||||
value = true,
|
||||
rememberType = rememberType,
|
||||
account = thisAccount,
|
||||
relay = signRelay,
|
||||
encryptedData = request.encryptedData,
|
||||
)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
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)
|
||||
|
||||
historyDatabase.dao().addHistory(
|
||||
listOf(
|
||||
HistoryEntity(
|
||||
id = 0,
|
||||
pkKey = localKey,
|
||||
type = SignerType.SIGN_EVENT.toString(),
|
||||
kind = localEvent.kind,
|
||||
time = TimeUtils.now(),
|
||||
accepted = isChecked,
|
||||
content = localEvent.toJson(),
|
||||
0,
|
||||
localKey,
|
||||
SignerType.CONNECT.toString(),
|
||||
null,
|
||||
TimeUtils.now(),
|
||||
isApproved,
|
||||
content = "",
|
||||
),
|
||||
),
|
||||
thisAccount.npub,
|
||||
)
|
||||
|
||||
BunkerRequestUtils.remove(request.request.id)
|
||||
|
||||
if (isChecked) {
|
||||
if (isApproved) {
|
||||
BunkerRequestUtils.sendBunkerResponse(
|
||||
context,
|
||||
thisAccount,
|
||||
request,
|
||||
BunkerResponse(request.request.id, localEvent.toJson(), null),
|
||||
BunkerResponse(request.request.id, "", null),
|
||||
application.application.relays,
|
||||
onLoading = {},
|
||||
onDone = {},
|
||||
@@ -456,93 +354,54 @@ fun BunkerMultiEventHomeScreen(
|
||||
onLoading = {},
|
||||
)
|
||||
}
|
||||
} else if (request.request is BunkerRequestConnect) {
|
||||
if (savedApplication == null) {
|
||||
dao.insertApplicationWithPermissions(application)
|
||||
|
||||
historyDatabase.dao().addHistory(
|
||||
listOf(
|
||||
HistoryEntity(
|
||||
0,
|
||||
localKey,
|
||||
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 {
|
||||
val type = requestType
|
||||
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) BunkerRequestUtils.getNip44v3Kind(request.request) else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
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,
|
||||
AmberUtils.acceptOrRejectPermission(
|
||||
application,
|
||||
localKey,
|
||||
type,
|
||||
permissionKind,
|
||||
isApproved,
|
||||
rememberType,
|
||||
thisAccount,
|
||||
encryptedData = request.encryptedData,
|
||||
decryptTypeScope = decryptTypeScope,
|
||||
)
|
||||
}
|
||||
|
||||
val signature = request.encryptedData?.result ?: continue
|
||||
BunkerRequestUtils.remove(request.request.id)
|
||||
if (isChecked) {
|
||||
dao.insertApplicationWithPermissions(application)
|
||||
|
||||
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(
|
||||
context,
|
||||
thisAccount,
|
||||
@@ -552,25 +411,26 @@ fun BunkerMultiEventHomeScreen(
|
||||
onLoading = {},
|
||||
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(
|
||||
context: Context,
|
||||
bunkerRequest: AmberBunkerRequest,
|
||||
checked: Boolean,
|
||||
onToggleChecked: () -> Unit,
|
||||
approved: Boolean,
|
||||
onApproveChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
val type = BunkerRequestUtils.getTypeFromBunker(bunkerRequest.request)
|
||||
var showDetails by remember { mutableStateOf(false) }
|
||||
@@ -659,42 +519,49 @@ private fun BunkerRequestCard(
|
||||
),
|
||||
border = BorderStroke(1.dp, Color.Gray),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggleChecked() },
|
||||
Column(
|
||||
Modifier.padding(8.dp),
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = { onToggleChecked() },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
uncheckedBorderColor = Color.Gray,
|
||||
),
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = if (checked) Color.Unspecified else Color.Gray,
|
||||
)
|
||||
if (preview.isNotBlank()) {
|
||||
Text(
|
||||
text = preview,
|
||||
color = Color.Gray,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
if (hasDetails) {
|
||||
RawJsonButton(
|
||||
onCLick = { showDetails = true },
|
||||
text = stringResource(R.string.show_details),
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp),
|
||||
) {
|
||||
Text(text = label)
|
||||
if (preview.isNotBlank()) {
|
||||
Text(
|
||||
text = preview,
|
||||
color = Color.Gray,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
if (hasDetails) {
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+198
-323
@@ -4,22 +4,16 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TriStateCheckbox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -32,7 +26,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
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.model.AmberEvent
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
@@ -82,6 +74,7 @@ fun IntentMultiEventHomeScreen(
|
||||
LaunchedEffect(Unit) {
|
||||
MultiEventScreenIntents.checkedStates.clear()
|
||||
MultiEventScreenIntents.rememberType = RememberType.NEVER
|
||||
// checkedStates now holds the per-request decision: true = Approve, false = Deny.
|
||||
intents.forEach { MultiEventScreenIntents.checkedStates[it.id] = true }
|
||||
}
|
||||
|
||||
@@ -99,30 +92,6 @@ fun IntentMultiEventHomeScreen(
|
||||
|
||||
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) {
|
||||
groupRequests(intents) {
|
||||
requestGroupKey(it.type, it.event?.kind, it.encryptedData, it.nip44v3Kind)
|
||||
@@ -136,19 +105,14 @@ fun IntentMultiEventHomeScreen(
|
||||
val expanded = groups.size == 1 || (expandedGroups[groupKey] ?: false)
|
||||
if (groups.size > 1) {
|
||||
item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") {
|
||||
val groupState = when {
|
||||
groupIntents.all { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.On
|
||||
groupIntents.none { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.Off
|
||||
else -> ToggleableState.Indeterminate
|
||||
}
|
||||
val groupApproved = groupIntents.all { MultiEventScreenIntents.checkedStates[it.id] ?: true }
|
||||
RequestGroupHeader(
|
||||
label = groupKey.toLabel(context),
|
||||
count = groupIntents.size,
|
||||
state = groupState,
|
||||
approved = groupApproved,
|
||||
expanded = expanded,
|
||||
onToggle = {
|
||||
val newValue = groupState != ToggleableState.On
|
||||
MultiEventScreenIntents.checkedStates.putAll(groupIntents.associate { it.id to newValue })
|
||||
onApproveChanged = { approve ->
|
||||
MultiEventScreenIntents.checkedStates.putAll(groupIntents.associate { it.id to approve })
|
||||
},
|
||||
onExpandToggle = {
|
||||
expandedGroups[groupKey] = !(expandedGroups[groupKey] ?: false)
|
||||
@@ -174,10 +138,9 @@ fun IntentMultiEventHomeScreen(
|
||||
IntentRequestCard(
|
||||
context = context,
|
||||
intent = intent,
|
||||
checked = MultiEventScreenIntents.checkedStates[intent.id] ?: true,
|
||||
onToggleChecked = {
|
||||
val current = MultiEventScreenIntents.checkedStates[intent.id] ?: true
|
||||
MultiEventScreenIntents.checkedStates[intent.id] = !current
|
||||
approved = MultiEventScreenIntents.checkedStates[intent.id] ?: true,
|
||||
onApproveChanged = {
|
||||
MultiEventScreenIntents.checkedStates[intent.id] = it
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -185,19 +148,16 @@ fun IntentMultiEventHomeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
AmberButton(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
AmberButton(
|
||||
Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors().copy(
|
||||
containerColor = orange,
|
||||
),
|
||||
onClick = {
|
||||
Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
|
||||
text = stringResource(R.string.confirm),
|
||||
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
|
||||
@@ -211,6 +171,7 @@ fun IntentMultiEventHomeScreen(
|
||||
} ?: continue
|
||||
|
||||
val dao = Amber.instance.dao(thisAccount.npub)
|
||||
val historyDatabase = Amber.instance.getHistoryDatabase(thisAccount.npub)
|
||||
val application = dao.getByKey(localKey) ?: ApplicationWithPermissions(
|
||||
application = ApplicationEntity(
|
||||
localKey,
|
||||
@@ -236,247 +197,163 @@ fun IntentMultiEventHomeScreen(
|
||||
}
|
||||
|
||||
var permissionsChanged = false
|
||||
val historyList = mutableListOf<HistoryEntity>()
|
||||
|
||||
for (intentData in accountIntents) {
|
||||
val isChecked = MultiEventScreenIntents.checkedStates[intentData.id] ?: true
|
||||
val groupKey = requestGroupKey(intentData.type, intentData.event?.kind, intentData.encryptedData, intentData.nip44v3Kind)
|
||||
val isApproved = 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 (rememberType != RememberType.NEVER && isChecked) {
|
||||
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 ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) {
|
||||
"*"
|
||||
|
||||
if (type == SignerType.SIGN_EVENT) {
|
||||
val localEvent = intentData.event!!
|
||||
|
||||
if (rememberType != RememberType.NEVER) {
|
||||
val signRelay = if (localEvent.kind == 22242) {
|
||||
if ((groupRelayAuthScopes[groupKey] ?: RelayAuthScope.SPECIFIC) == RelayAuthScope.ALL) {
|
||||
"*"
|
||||
} else {
|
||||
RelayUrlUtils.extractHostAndPort(AmberEvent.relay(localEvent))
|
||||
}
|
||||
} 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 {
|
||||
""
|
||||
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()) {
|
||||
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)
|
||||
} 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(
|
||||
context: Context,
|
||||
intent: IntentData,
|
||||
checked: Boolean,
|
||||
onToggleChecked: () -> Unit,
|
||||
approved: Boolean,
|
||||
onApproveChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
val type = intent.type
|
||||
var showDetails by remember { mutableStateOf(false) }
|
||||
@@ -569,42 +446,49 @@ private fun IntentRequestCard(
|
||||
),
|
||||
border = BorderStroke(1.dp, Color.Gray),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggleChecked() },
|
||||
Column(
|
||||
Modifier.padding(8.dp),
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = { onToggleChecked() },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
uncheckedBorderColor = Color.Gray,
|
||||
),
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = if (checked) Color.Unspecified else Color.Gray,
|
||||
)
|
||||
if (preview.isNotBlank()) {
|
||||
Text(
|
||||
text = preview,
|
||||
color = Color.Gray,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
if (hasDetails) {
|
||||
RawJsonButton(
|
||||
onCLick = { showDetails = true },
|
||||
text = stringResource(R.string.show_details),
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp),
|
||||
) {
|
||||
Text(text = label)
|
||||
if (preview.isNotBlank()) {
|
||||
Text(
|
||||
text = preview,
|
||||
color = Color.Gray,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
if (hasDetails) {
|
||||
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(
|
||||
results: MutableList<Result>,
|
||||
) {
|
||||
|
||||
@@ -63,9 +63,10 @@ fun LabeledBorderBox(
|
||||
fun RememberMyChoiceToggles(
|
||||
selected: RememberType,
|
||||
onSelected: (RememberType) -> Unit,
|
||||
label: String = stringResource(R.string.automatically_sign_this_for),
|
||||
) {
|
||||
LabeledBorderBox(
|
||||
label = stringResource(R.string.automatically_sign_this_for),
|
||||
label = label,
|
||||
) {
|
||||
AmberToggles(
|
||||
selected = selected,
|
||||
|
||||
@@ -12,14 +12,13 @@ import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TriStateCheckbox
|
||||
import androidx.compose.runtime.Composable
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind
|
||||
@@ -226,6 +225,7 @@ fun RequestGroupOptions(
|
||||
RememberMyChoiceToggles(
|
||||
selected = rememberType,
|
||||
onSelected = onRememberTypeChanged,
|
||||
label = stringResource(R.string.remember_my_choice_for),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -234,36 +234,51 @@ fun RequestGroupOptions(
|
||||
fun RequestGroupHeader(
|
||||
label: String,
|
||||
count: Int,
|
||||
state: ToggleableState,
|
||||
approved: Boolean,
|
||||
expanded: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
onApproveChanged: (Boolean) -> Unit,
|
||||
onExpandToggle: () -> Unit,
|
||||
) {
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (expanded) 180f else 0f,
|
||||
label = "group header chevron rotation",
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onExpandToggle() },
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
TriStateCheckbox(
|
||||
state = state,
|
||||
onClick = onToggle,
|
||||
)
|
||||
Text(
|
||||
text = "$label ($count)",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
contentDescription = null,
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.rotate(rotation),
|
||||
.fillMaxWidth()
|
||||
.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(
|
||||
text: String,
|
||||
isSelected: Boolean,
|
||||
selectedTextColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
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),
|
||||
label = "textColor",
|
||||
)
|
||||
@@ -59,12 +60,14 @@ fun ToggleOptionPreview() {
|
||||
ToggleOption(
|
||||
text = "Always",
|
||||
isSelected = true,
|
||||
selectedTextColor = Color.Black,
|
||||
modifier = Modifier.width(80.dp),
|
||||
onClick = {},
|
||||
)
|
||||
ToggleOption(
|
||||
text = "Never",
|
||||
isSelected = false,
|
||||
selectedTextColor = Color.Black,
|
||||
modifier = Modifier.width(80.dp),
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
@@ -630,8 +630,6 @@
|
||||
<string name="pubkey">Pubkey</string>
|
||||
<string name="date">Datum</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="discard_all">Auswahl verwerfen</string>
|
||||
<string name="approve_all">Auswahl genehmigen</string>
|
||||
<string name="requests_message">Anfragen %1$s</string>
|
||||
<string name="trust_excellent">Ausgezeichnet</string>
|
||||
<string name="trust_good">Gut</string>
|
||||
|
||||
@@ -633,8 +633,6 @@
|
||||
<string name="pubkey">Clave pública</string>
|
||||
<string name="date">Fecha</string>
|
||||
<string name="tags">Etiquetas</string>
|
||||
<string name="discard_all">Descartar seleccionados</string>
|
||||
<string name="approve_all">Aprobar seleccionados</string>
|
||||
<string name="requests_message">Solicitudes %1$s</string>
|
||||
<string name="trust_excellent">Excelente</string>
|
||||
<string name="trust_good">Bueno</string>
|
||||
|
||||
@@ -630,8 +630,6 @@
|
||||
<string name="pubkey">Pubkey</string>
|
||||
<string name="date">Date</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="discard_all">Rejeter la sélection</string>
|
||||
<string name="approve_all">Approuver la sélection</string>
|
||||
<string name="requests_message">Requêtes %1$s</string>
|
||||
<string name="trust_excellent">Excellent</string>
|
||||
<string name="trust_good">Bon</string>
|
||||
|
||||
@@ -633,8 +633,6 @@
|
||||
<string name="pubkey">Pubkey</string>
|
||||
<string name="date">Tanggal</string>
|
||||
<string name="tags">Tag</string>
|
||||
<string name="discard_all">Buang yang dipilih</string>
|
||||
<string name="approve_all">Setujui yang dipilih</string>
|
||||
<string name="requests_message">Permintaan %1$s</string>
|
||||
<string name="trust_excellent">Sangat baik</string>
|
||||
<string name="trust_good">Baik</string>
|
||||
|
||||
@@ -633,8 +633,6 @@
|
||||
<string name="pubkey">Chiave pubblica</string>
|
||||
<string name="date">Data</string>
|
||||
<string name="tags">Tag</string>
|
||||
<string name="discard_all">Scarta selezionati</string>
|
||||
<string name="approve_all">Approva selezionati</string>
|
||||
<string name="requests_message">Richieste %1$s</string>
|
||||
<string name="trust_excellent">Eccellente</string>
|
||||
<string name="trust_good">Buono</string>
|
||||
|
||||
@@ -609,8 +609,6 @@
|
||||
<string name="pubkey">Pubkey</string>
|
||||
<string name="date">日付</string>
|
||||
<string name="tags">タグ</string>
|
||||
<string name="discard_all">選択した項目を破棄</string>
|
||||
<string name="approve_all">選択した項目を承認</string>
|
||||
<string name="requests_message">リクエスト %1$s</string>
|
||||
<string name="trust_excellent">非常に高い</string>
|
||||
<string name="trust_good">高い</string>
|
||||
|
||||
@@ -633,8 +633,6 @@
|
||||
<string name="pubkey">공개 키</string>
|
||||
<string name="date">날짜</string>
|
||||
<string name="tags">태그</string>
|
||||
<string name="discard_all">선택 항목 버리기</string>
|
||||
<string name="approve_all">선택 항목 승인</string>
|
||||
<string name="requests_message">요청 %1$s</string>
|
||||
<string name="trust_excellent">매우 좋음</string>
|
||||
<string name="trust_good">좋음</string>
|
||||
|
||||
@@ -628,8 +628,6 @@
|
||||
<string name="pubkey">Chave</string>
|
||||
<string name="date">Data</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="discard_all">Descartar selecionado</string>
|
||||
<string name="approve_all">Aprovar selecionados</string>
|
||||
<string name="requests_message">Solicitações %1$s</string>
|
||||
<string name="trust_excellent">Excelente</string>
|
||||
<string name="trust_good">Bom</string>
|
||||
|
||||
@@ -633,8 +633,6 @@
|
||||
<string name="pubkey">Публичный ключ</string>
|
||||
<string name="date">Дата</string>
|
||||
<string name="tags">Теги</string>
|
||||
<string name="discard_all">Отклонить выбранные</string>
|
||||
<string name="approve_all">Одобрить выбранные</string>
|
||||
<string name="requests_message">Запросы %1$s</string>
|
||||
<string name="trust_excellent">Отлично</string>
|
||||
<string name="trust_good">Хорошо</string>
|
||||
|
||||
@@ -609,8 +609,6 @@
|
||||
<string name="pubkey">Pubkey</string>
|
||||
<string name="date">วันที่</string>
|
||||
<string name="tags">แท็ก</string>
|
||||
<string name="discard_all">ยกเลิกรายการที่เลือก</string>
|
||||
<string name="approve_all">อนุมัติรายการที่เลือก</string>
|
||||
<string name="requests_message">คำขอ %1$s</string>
|
||||
<string name="trust_excellent">ยอดเยี่ยม</string>
|
||||
<string name="trust_good">ดี</string>
|
||||
|
||||
@@ -629,8 +629,6 @@
|
||||
<string name="pubkey">Pubkey (Açık anahtar)</string>
|
||||
<string name="date">Tarih</string>
|
||||
<string name="tags">Etiketler</string>
|
||||
<string name="discard_all">Seçilenleri yoksay</string>
|
||||
<string name="approve_all">Seçilenleri onayla</string>
|
||||
<string name="requests_message">İstekler: %1$s</string>
|
||||
<string name="trust_excellent">Mükemmel</string>
|
||||
<string name="trust_good">İyi</string>
|
||||
|
||||
@@ -609,8 +609,6 @@
|
||||
<string name="pubkey">Pubkey</string>
|
||||
<string name="date">Ngày</string>
|
||||
<string name="tags">Thẻ</string>
|
||||
<string name="discard_all">Bỏ qua đã chọn</string>
|
||||
<string name="approve_all">Phê duyệt đã chọn</string>
|
||||
<string name="requests_message">Yêu cầu %1$s</string>
|
||||
<string name="trust_excellent">Xuất sắc</string>
|
||||
<string name="trust_good">Tốt</string>
|
||||
|
||||
@@ -614,8 +614,6 @@
|
||||
<string name="pubkey">公钥</string>
|
||||
<string name="date">日期</string>
|
||||
<string name="tags">标签</string>
|
||||
<string name="discard_all">拒绝所选</string>
|
||||
<string name="approve_all">批准所选</string>
|
||||
<string name="requests_message">请求 %1$s</string>
|
||||
<string name="trust_excellent">极佳</string>
|
||||
<string name="trust_good">良好</string>
|
||||
|
||||
@@ -661,8 +661,9 @@
|
||||
<string name="pubkey">Pubkey</string>
|
||||
<string name="date">Date</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="discard_all">Discard selected</string>
|
||||
<string name="approve_all">Approve selected</string>
|
||||
<string name="approve">Approve</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="trust_excellent">Excellent</string>
|
||||
<string name="trust_good">Good</string>
|
||||
|
||||
Reference in New Issue
Block a user