diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt index 7f55a7cd..20888c74 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt @@ -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 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 AmberToggles( .width(segmentWidth) .fillMaxHeight() .clip(RoundedCornerShape(4.dp)) - .background(MaterialTheme.colorScheme.primary), + .background(indicatorColor(selected)), ) Row(modifier = Modifier.fillMaxHeight()) { @@ -109,6 +112,7 @@ fun AmberToggles( modifier = Modifier.width(segmentWidth), text = label(option), isSelected = option == selected, + selectedTextColor = selectedTextColor(option), onClick = { onSelected(option) }, ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index fbcb3051..7b984686 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -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 + }, + ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index bb914703..19480725 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -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() 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() + 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() - 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() - - 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, -) { - 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, ) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt index d5b120f6..9a915e5a 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt @@ -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, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt index 16f06fce..b0ad7463 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt @@ -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 + }, ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ToogleOption.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ToogleOption.kt index fc0ce4ee..8d94375d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ToogleOption.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ToogleOption.kt @@ -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 = {}, ) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 57c54c56..07f7bc6a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -630,8 +630,9 @@ Pubkey Datum Tags - Auswahl verwerfen - Auswahl genehmigen + Genehmigen + Bestätigen + Meine Wahl speichern für Anfragen %1$s Ausgezeichnet Gut diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b81d4e4c..9b93266c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -633,8 +633,9 @@ Clave pública Fecha Etiquetas - Descartar seleccionados - Aprobar seleccionados + Aprobar + Confirmar + Recordar mi elección para Solicitudes %1$s Excelente Bueno diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6d7793d2..1b4408e3 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -630,8 +630,9 @@ Pubkey Date Tags - Rejeter la sélection - Approuver la sélection + Approuver + Confirmer + Mémoriser mon choix pour Requêtes %1$s Excellent Bon diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 2d7c8f45..fbcff96d 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -633,8 +633,9 @@ Pubkey Tanggal Tag - Buang yang dipilih - Setujui yang dipilih + Setujui + Konfirmasi + Ingat pilihan saya untuk Permintaan %1$s Sangat baik Baik diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 83359f07..312f810a 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -633,8 +633,9 @@ Chiave pubblica Data Tag - Scarta selezionati - Approva selezionati + Approva + Conferma + Ricorda la mia scelta per Richieste %1$s Eccellente Buono diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 86a5e358..30c4ea0b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -609,8 +609,9 @@ Pubkey 日付 タグ - 選択した項目を破棄 - 選択した項目を承認 + 承認 + 確認 + この選択を記憶する対象 リクエスト %1$s 非常に高い 高い diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 1beb64fa..feadfa10 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -633,8 +633,9 @@ 공개 키 날짜 태그 - 선택 항목 버리기 - 선택 항목 승인 + 승인 + 확인 + 내 선택 기억 요청 %1$s 매우 좋음 좋음 diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 024bc035..5fc43a91 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -628,8 +628,9 @@ Chave Data Tags - Descartar selecionado - Aprovar selecionados + Aprovar + Confirmar + Lembrar minha escolha para Solicitações %1$s Excelente Bom diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 562dae5f..dab032fe 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -633,8 +633,9 @@ Публичный ключ Дата Теги - Отклонить выбранные - Одобрить выбранные + Одобрить + Подтвердить + Запомнить мой выбор для Запросы %1$s Отлично Хорошо diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 01373070..d1f23640 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -609,8 +609,9 @@ Pubkey วันที่ แท็ก - ยกเลิกรายการที่เลือก - อนุมัติรายการที่เลือก + อนุมัติ + ยืนยัน + จดจำตัวเลือกของฉันสำหรับ คำขอ %1$s ยอดเยี่ยม ดี diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1d72f8ea..e4c7b5b5 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -629,8 +629,9 @@ Pubkey (Açık anahtar) Tarih Etiketler - Seçilenleri yoksay - Seçilenleri onayla + Onayla + Doğrula + Seçimimi şu için hatırla İstekler: %1$s Mükemmel İyi diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml index c8f2799b..829819ba 100644 --- a/app/src/main/res/values-vi-rVN/strings.xml +++ b/app/src/main/res/values-vi-rVN/strings.xml @@ -609,8 +609,9 @@ Pubkey Ngày Thẻ - Bỏ qua đã chọn - Phê duyệt đã chọn + Phê duyệt + Xác nhận + Ghi nhớ lựa chọn của tôi cho Yêu cầu %1$s Xuất sắc Tốt diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 185d2231..0330d6d4 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -614,8 +614,9 @@ 公钥 日期 标签 - 拒绝所选 - 批准所选 + 批准 + 确认 + 为此记住我的选择 请求 %1$s 极佳 良好 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd8bc69a..a854e113 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -661,8 +661,9 @@ Pubkey Date Tags - Discard selected - Approve selected + Approve + Confirm + Remember my choice for Requests %1$s Excellent Good