diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt index 89df381160..87d197065b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt @@ -68,8 +68,7 @@ class TrustedRelayListState( .stateIn( scope, SharingStarted.Eagerly, - // Synchronously seed from the cached (pre-decrypted) backup so the relay-auth - // ledger sees a non-empty list immediately, before the IO-thread onStart emit. + // Synchronously seed public tags from the backup; private tags may be absent on first boot. settings.backupTrustedRelayList?.let { decryptionCache.cachedRelays(it) } ?: emptySet(), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt new file mode 100644 index 0000000000..f36d4f5b59 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.napplet + +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent +import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest +import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent + +/** + * Looks up the best-effort human title and icon URL for a napplet from the local cache. + * Falls back to the d-identifier or [untitled] when no manifest is cached. + */ +fun resolveNappletMeta( + author: String, + identifier: String, + untitled: String, +): Pair { + val events = + Amethyst.instance.cache + .filter(Filter(kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND), authors = listOf(author))) + .mapNotNull { it.event } + val match = + events.firstOrNull { ev -> + when (ev) { + is NamedNappletEvent -> ev.identifier() == identifier + is RootNappletEvent -> identifier.isEmpty() + else -> false + } + } as? NappletManifest + val title = match?.title()?.ifBlank { null } ?: identifier.ifBlank { untitled } + val iconUrl = match?.icon()?.ifBlank { null } + return title to iconUrl +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletSignerConsentActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletSignerConsentActivity.kt index 80e786fe4d..a6f3a144e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletSignerConsentActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletSignerConsentActivity.kt @@ -57,7 +57,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon @@ -65,10 +64,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.napplet.signers.SignerOpGrant import com.vitorpamplona.amethyst.ui.theme.AmethystTheme -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent -import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest -import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import com.vitorpamplona.quartz.utils.TimeUtils class NappletSignerConsentActivity : ComponentActivity() { @@ -110,24 +105,6 @@ class NappletSignerConsentActivity : ComponentActivity() { } } -private fun resolveIconUrl(coordinate: String): String? { - val author = coordinate.substringBefore(':') - val identifier = coordinate.substringAfter(':', "") - val events = - Amethyst.instance.cache - .filter(Filter(kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND), authors = listOf(author))) - .mapNotNull { it.event } - val match = - events.firstOrNull { ev -> - when (ev) { - is NamedNappletEvent -> ev.identifier() == identifier - is RootNappletEvent -> identifier.isEmpty() - else -> false - } - } as? NappletManifest - return match?.icon()?.ifBlank { null } -} - @Composable private fun NappletSignerConsentDialog( info: NappletSignerConsentInfo, @@ -138,7 +115,10 @@ private fun NappletSignerConsentDialog( var showMoreOptions by remember { mutableStateOf(false) } val scrollState = rememberScrollState() val maxHeight = LocalConfiguration.current.screenHeightDp.dp * 0.85f - val iconUrl = remember(info.coordinate) { resolveIconUrl(info.coordinate) } + val iconUrl = + remember(info.coordinate) { + resolveNappletMeta(info.coordinate.substringBefore(':'), info.coordinate.substringAfter(':', ""), "").second + } Dialog( onDismissRequest = onDismiss, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt index 3945cbc94d..3268bfe0b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -32,7 +30,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -59,12 +56,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.napplet.NappletCapability import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity @@ -78,13 +73,10 @@ import com.vitorpamplona.amethyst.napplet.DataStoreNappletPermissionStore import com.vitorpamplona.amethyst.napplet.DataStoreNostrSignerPermissionStore import com.vitorpamplona.amethyst.napplet.descriptionRes import com.vitorpamplona.amethyst.napplet.labelRes +import com.vitorpamplona.amethyst.napplet.resolveNappletMeta import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent -import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest -import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -288,21 +280,21 @@ private fun PolicyPicker( onSelect: (AppSignerPolicy) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - PolicyOption( + PolicyCard( selected = selected == AppSignerPolicy.FULL_TRUST, symbol = MaterialSymbols.Favorite, label = stringResource(R.string.napplet_policy_full_trust), description = stringResource(R.string.napplet_policy_full_trust_desc), onClick = { onSelect(AppSignerPolicy.FULL_TRUST) }, ) - PolicyOption( + PolicyCard( selected = selected == AppSignerPolicy.REASONABLE, symbol = MaterialSymbols.Shield, label = stringResource(R.string.napplet_policy_reasonable), description = stringResource(R.string.napplet_policy_reasonable_desc), onClick = { onSelect(AppSignerPolicy.REASONABLE) }, ) - PolicyOption( + PolicyCard( selected = selected == AppSignerPolicy.PARANOID, symbol = MaterialSymbols.Lock, label = stringResource(R.string.napplet_policy_paranoid), @@ -312,52 +304,6 @@ private fun PolicyPicker( } } -@Composable -private fun PolicyOption( - selected: Boolean, - symbol: MaterialSymbol, - label: String, - description: String, - onClick: () -> Unit, -) { - val borderColor = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - val bgColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f) else MaterialTheme.colorScheme.surface - - Surface( - modifier = - Modifier - .fillMaxWidth() - .border(width = if (selected) 2.dp else 1.dp, color = borderColor, shape = RoundedCornerShape(12.dp)) - .clickable(onClick = onClick), - shape = RoundedCornerShape(12.dp), - color = bgColor, - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - symbol = symbol, - contentDescription = null, - tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(28.dp), - ) - Column(modifier = Modifier.weight(1f)) { - Text(label, style = MaterialTheme.typography.titleSmall) - Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - if (selected) { - Icon( - symbol = MaterialSymbols.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - } - } - } -} - @Composable private fun OpOverrideRow( opKey: String, @@ -411,7 +357,7 @@ private fun CapabilityDetailRow( modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), ) { Icon( - capability.capSymbol(), + capability.symbol(), contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(22.dp), @@ -467,21 +413,6 @@ private fun NostrOpDecision.decisionLabel(): String = NostrOpDecision.DENY -> stringResource(R.string.napplet_decision_deny) } -private fun NappletCapability.capSymbol(): MaterialSymbol = - when (this) { - NappletCapability.SHELL -> MaterialSymbols.Tune - NappletCapability.IDENTITY -> MaterialSymbols.AccountCircle - NappletCapability.KEYS -> MaterialSymbols.Key - NappletCapability.RELAY -> MaterialSymbols.Public - NappletCapability.STORAGE -> MaterialSymbols.Storage - NappletCapability.VALUE -> MaterialSymbols.Bolt - NappletCapability.RESOURCE -> MaterialSymbols.Language - NappletCapability.UPLOAD -> MaterialSymbols.Upload - NappletCapability.THEME -> MaterialSymbols.Image - NappletCapability.NOTIFY -> MaterialSymbols.Notifications - NappletCapability.INC -> MaterialSymbols.SwapHoriz - } - private suspend fun loadDetailState( coordinate: String, capabilityLedger: NappletPermissionLedger, @@ -502,7 +433,7 @@ private suspend fun loadDetailState( val signerPolicy = signerLedger.store.loadPolicy(coordinate) val opOverrides = signerLedger.store.allOpDecisions(coordinate) - val (title, iconUrl) = resolveAppMeta(author, identifier, untitled) + val (title, iconUrl) = resolveNappletMeta(author, identifier, untitled) return ConnectedAppDetailState( title = title, coordinate = coordinate, @@ -512,25 +443,3 @@ private suspend fun loadDetailState( capabilities = capGrants, ) } - -private fun resolveAppMeta( - author: String, - identifier: String, - untitled: String, -): Pair { - val events = - Amethyst.instance.cache - .filter(Filter(kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND), authors = listOf(author))) - .mapNotNull { it.event } - val match = - events.firstOrNull { ev -> - when (ev) { - is NamedNappletEvent -> ev.identifier() == identifier - is RootNappletEvent -> identifier.isEmpty() - else -> false - } - } as? NappletManifest - val title = match?.title()?.ifBlank { null } ?: identifier.ifBlank { untitled } - val iconUrl = match?.icon()?.ifBlank { null } - return title to iconUrl -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt index 3be6479b24..3dc8e33fd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon @@ -63,14 +62,11 @@ import com.vitorpamplona.amethyst.commons.napplet.signers.AppSignerPolicy import com.vitorpamplona.amethyst.commons.napplet.signers.NostrSignerPermissionLedger import com.vitorpamplona.amethyst.napplet.DataStoreNappletPermissionStore import com.vitorpamplona.amethyst.napplet.DataStoreNostrSignerPermissionStore +import com.vitorpamplona.amethyst.napplet.resolveNappletMeta import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent -import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest -import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -229,7 +225,7 @@ private suspend fun loadConnectedApps( .map { coordinate -> val author = coordinate.substringBefore(':') val identifier = coordinate.substringAfter(':', "") - val (title, iconUrl) = resolveAppMeta(author, identifier, untitled) + val (title, iconUrl) = resolveNappletMeta(author, identifier, untitled) ConnectedAppEntry( coordinate = coordinate, title = title, @@ -240,25 +236,3 @@ private suspend fun loadConnectedApps( ) }.sortedBy { it.title.lowercase() } } - -private fun resolveAppMeta( - author: String, - identifier: String, - untitled: String, -): Pair { - val events = - Amethyst.instance.cache - .filter(Filter(kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND), authors = listOf(author))) - .mapNotNull { it.event } - val match = - events.firstOrNull { ev -> - when (ev) { - is NamedNappletEvent -> ev.identifier() == identifier - is RootNappletEvent -> identifier.isEmpty() - else -> false - } - } as? NappletManifest - val title = match?.title()?.ifBlank { null } ?: identifier.ifBlank { untitled } - val iconUrl = match?.icon()?.ifBlank { null } - return title to iconUrl -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletCapabilityExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletCapabilityExt.kt new file mode 100644 index 0000000000..66bce77ba8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletCapabilityExt.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets + +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.napplet.NappletCapability + +internal fun NappletCapability.symbol(): MaterialSymbol = + when (this) { + NappletCapability.SHELL -> MaterialSymbols.Tune + NappletCapability.IDENTITY -> MaterialSymbols.AccountCircle + NappletCapability.KEYS -> MaterialSymbols.Key + NappletCapability.RELAY -> MaterialSymbols.Public + NappletCapability.STORAGE -> MaterialSymbols.Storage + NappletCapability.VALUE -> MaterialSymbols.Bolt + NappletCapability.RESOURCE -> MaterialSymbols.Language + NappletCapability.UPLOAD -> MaterialSymbols.Upload + NappletCapability.THEME -> MaterialSymbols.Image + NappletCapability.NOTIFY -> MaterialSymbols.Notifications + NappletCapability.INC -> MaterialSymbols.SwapHoriz + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletPermissionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletPermissionsScreen.kt index 7637ae5a49..b9df14ea2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletPermissionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletPermissionsScreen.kt @@ -58,10 +58,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.napplet.NappletCapability import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity @@ -70,13 +68,10 @@ import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionL import com.vitorpamplona.amethyst.napplet.DataStoreNappletPermissionStore import com.vitorpamplona.amethyst.napplet.descriptionRes import com.vitorpamplona.amethyst.napplet.labelRes +import com.vitorpamplona.amethyst.napplet.resolveNappletMeta import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent -import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest -import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -304,44 +299,7 @@ private suspend fun loadGrants( val identifier = coordinate.substringAfter(':', "") NappletGrantsUi( identity = NappletIdentity(authorPubKey = author, identifier = identifier), - title = resolveTitle(author, identifier, untitled), + title = resolveNappletMeta(author, identifier, untitled).first, capabilities = caps.entries.sortedBy { it.key.ordinal }.map { it.key to it.value }, ) }.sortedBy { it.title.lowercase() } - -/** Best-effort human title from a cached manifest; falls back to the d-identifier or [untitled]. */ -private fun resolveTitle( - author: String, - identifier: String, - untitled: String, -): String { - val events = - Amethyst.instance.cache - .filter(Filter(kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND), authors = listOf(author))) - .mapNotNull { it.event } - val match = - events.firstOrNull { ev -> - when (ev) { - is NamedNappletEvent -> ev.identifier() == identifier - is RootNappletEvent -> identifier.isEmpty() - else -> false - } - } - return (match as? NappletManifest)?.title()?.ifBlank { null } - ?: identifier.ifBlank { untitled } -} - -private fun NappletCapability.symbol(): MaterialSymbol = - when (this) { - NappletCapability.SHELL -> MaterialSymbols.Tune - NappletCapability.IDENTITY -> MaterialSymbols.AccountCircle - NappletCapability.KEYS -> MaterialSymbols.Key - NappletCapability.RELAY -> MaterialSymbols.Public - NappletCapability.STORAGE -> MaterialSymbols.Storage - NappletCapability.VALUE -> MaterialSymbols.Bolt - NappletCapability.RESOURCE -> MaterialSymbols.Language - NappletCapability.UPLOAD -> MaterialSymbols.Upload - NappletCapability.THEME -> MaterialSymbols.Image - NappletCapability.NOTIFY -> MaterialSymbols.Notifications - NappletCapability.INC -> MaterialSymbols.SwapHoriz - } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/PolicyCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/PolicyCard.kt new file mode 100644 index 0000000000..1fa058d801 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/PolicyCard.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets + +import androidx.compose.foundation.border +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.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols + +/** Bordered selection card used in policy-picker UIs (napplet trust level, relay auth). */ +@Composable +fun PolicyCard( + selected: Boolean, + symbol: MaterialSymbol, + label: String, + description: String, + onClick: () -> Unit, +) { + val borderColor = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + val bgColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f) else MaterialTheme.colorScheme.surface + + Surface( + modifier = + Modifier + .fillMaxWidth() + .border(width = if (selected) 2.dp else 1.dp, color = borderColor, shape = RoundedCornerShape(12.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(12.dp), + color = bgColor, + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + symbol = symbol, + contentDescription = null, + tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(28.dp), + ) + Column(modifier = Modifier.weight(1f)) { + Text(label, style = MaterialTheme.typography.titleSmall) + Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + if (selected) { + Icon( + symbol = MaterialSymbols.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt index 88272e4418..b228c4812e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relayauth -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,9 +29,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton @@ -60,7 +56,6 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy @@ -69,6 +64,7 @@ import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.RelayAut import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.PolicyCard import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -136,7 +132,7 @@ fun RelayAuthSettingsScreen( PolicyCard( selected = globalPolicy == policy, symbol = symbol, - title = stringResource(titleRes), + label = stringResource(titleRes), description = stringResource(descRes), onClick = { account.settings.changeDefaultRelayAuthPolicy(policy) }, ) @@ -200,52 +196,6 @@ fun RelayAuthSettingsScreen( } } -@Composable -private fun PolicyCard( - selected: Boolean, - symbol: MaterialSymbol, - title: String, - description: String, - onClick: () -> Unit, -) { - val borderColor = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - val bgColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f) else MaterialTheme.colorScheme.surface - - Surface( - modifier = - Modifier - .fillMaxWidth() - .border(width = if (selected) 2.dp else 1.dp, color = borderColor, shape = RoundedCornerShape(12.dp)) - .clickable(onClick = onClick), - shape = RoundedCornerShape(12.dp), - color = bgColor, - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - symbol = symbol, - contentDescription = null, - tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(28.dp), - ) - Column(modifier = Modifier.weight(1f)) { - Text(title, style = MaterialTheme.typography.titleSmall) - Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - if (selected) { - Icon( - symbol = MaterialSymbols.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - } - } - } -} - @Composable private fun PerRelayOverrideRow( url: String, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedger.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedger.kt index 69fd4c03c4..2b3a40c27b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedger.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/signers/NostrSignerPermissionLedger.kt @@ -118,9 +118,9 @@ class NostrSignerPermissionLedger( when (op) { is NostrSignerOp.SignKind -> when (op.kind) { - 1 -> NostrOpDecision.ALLOW // Short text notes: common, low-risk - 6 -> NostrOpDecision.ALLOW // Reposts - 7 -> NostrOpDecision.ALLOW // Reactions / emoji + 1 -> NostrOpDecision.ALLOW + 6 -> NostrOpDecision.ALLOW + 7 -> NostrOpDecision.ALLOW else -> NostrOpDecision.ASK } NostrSignerOp.Encrypt -> NostrOpDecision.ALLOW