diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt index d0937b37b2..497a7a143e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.napplethost.NappletBlobCache import com.vitorpamplona.amethyst.napplethost.NappletBlobPrefetcher @@ -107,3 +108,19 @@ private fun resolveIconBlob(event: Event?): IconBlob? = is NamedSiteEvent -> event.iconBlob()?.let { IconBlob(it, event.servers()) } else -> null } + +/** + * A Coil model (`file://…`) for the cached favicon of [url]'s host, or null when no favicon + * has been captured yet. The favicon is stored by [BrowserIconRegistry] at browse time (the + * WebView captures it in the sandboxed `:napplet` process); this composable just reads the cache. + * + * Early-returns null when [url] is blank or has no parseable host — this early return is stable + * for a given [url] (the host either always parses or never does), so composition structure is + * preserved across recompositions. + */ +@Composable +fun rememberWebAppIconModel(url: String): String? { + val host = remember(url) { OmniboxInput.hostOf(url) } ?: return null + val iconKeys by BrowserIconRegistry.keys.collectAsStateWithLifecycle() + return remember(host, iconKeys) { BrowserIconRegistry.iconModelFor(host) } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConnectActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConnectActivity.kt index b68aa5eaea..a5c2f15811 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConnectActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConnectActivity.kt @@ -146,8 +146,14 @@ private fun NappletConnectScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { + val isBrowser = info.coordinate.startsWith("browser:") FavoriteAppIcon( - app = FavoriteApp.NostrApp(info.coordinate, info.appletTitle, 0L, info.iconUrl), + app = + if (isBrowser) { + FavoriteApp.WebApp(info.coordinate.substringAfter(':'), info.appletTitle, 0L, info.iconUrl) + } else { + FavoriteApp.NostrApp(info.coordinate, info.appletTitle, 0L, info.iconUrl) + }, tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.size(56.dp), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentActivity.kt index ff4a11915a..94875be170 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentActivity.kt @@ -138,8 +138,14 @@ private fun NappletConsentDialog( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { + val isBrowser = info.coordinate.startsWith("browser:") FavoriteAppIcon( - app = FavoriteApp.NostrApp(info.coordinate, info.appletTitle, 0L, info.iconUrl), + app = + if (isBrowser) { + FavoriteApp.WebApp(info.coordinate.substringAfter(':'), info.appletTitle, 0L, info.iconUrl) + } else { + FavoriteApp.NostrApp(info.coordinate, info.appletTitle, 0L, info.iconUrl) + }, tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.size(56.dp), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentSummary.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentSummary.kt index 888557adf2..a65238c703 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentSummary.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentSummary.kt @@ -22,9 +22,11 @@ package com.vitorpamplona.amethyst.napplet import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.napplet.NappletCapability import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest +import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.quartz.lightning.LnInvoiceUtil @@ -42,7 +44,13 @@ class NappletConsentSummary( request: NappletRequest, ): NappletConsentInfo { val untitled = context.getString(R.string.napplet_fallback_title, identity.authorPubKey.take(8)) - val (title, iconUrl) = resolveNappletMeta(identity.authorPubKey, identity.identifier, untitled) + val (title, iconUrl) = + if (identity.authorPubKey == "browser") { + val host = OmniboxInput.hostOf(identity.identifier) ?: identity.identifier + host to BrowserIconRegistry.iconModelFor(host) + } else { + resolveNappletMeta(identity.authorPubKey, identity.identifier, untitled) + } return NappletConsentInfo( appletTitle = title, coordinate = identity.coordinate, 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 74318dd06e..8dd8ed0e88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletSignerConsentActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletSignerConsentActivity.kt @@ -143,8 +143,14 @@ private fun NappletSignerConsentDialog( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), ) { + val isBrowser = info.coordinate.startsWith("browser:") FavoriteAppIcon( - app = FavoriteApp.NostrApp(info.coordinate, info.appletTitle, 0L, info.iconUrl), + app = + if (isBrowser) { + FavoriteApp.WebApp(info.coordinate.substringAfter(':'), info.appletTitle, 0L, info.iconUrl) + } else { + FavoriteApp.NostrApp(info.coordinate, info.appletTitle, 0L, info.iconUrl) + }, tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.size(56.dp), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt index 9b1714404f..9c7bb146d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt @@ -22,9 +22,11 @@ package com.vitorpamplona.amethyst.napplet import android.content.Context import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest import com.vitorpamplona.amethyst.commons.napplet.signers.NostrSignerOp +import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindNameFor import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate @@ -46,7 +48,13 @@ fun buildSignerConsentInfo( request: NappletRequest, ): NappletSignerConsentInfo { val untitled = context.getString(R.string.napplet_fallback_title, identity.authorPubKey.take(8)) - val (title, iconUrl) = resolveNappletMeta(identity.authorPubKey, identity.identifier, untitled) + val (title, iconUrl) = + if (identity.authorPubKey == "browser") { + val host = OmniboxInput.hostOf(identity.identifier) ?: identity.identifier + host to BrowserIconRegistry.iconModelFor(host) + } else { + resolveNappletMeta(identity.authorPubKey, identity.identifier, untitled) + } val summary = op.label(context) val preview = when (request) { @@ -93,7 +101,18 @@ fun buildConnectInfo( identity: NappletIdentity, ): NappletConnectInfo { val untitled = context.getString(R.string.napplet_fallback_title, identity.authorPubKey.take(8)) - val (title, iconUrl) = resolveNappletMeta(identity.authorPubKey, identity.identifier, untitled) - val domain = identity.identifier.ifBlank { identity.authorPubKey.take(12) + "…" } + val (title, iconUrl) = + if (identity.authorPubKey == "browser") { + val host = OmniboxInput.hostOf(identity.identifier) ?: identity.identifier + host to BrowserIconRegistry.iconModelFor(host) + } else { + resolveNappletMeta(identity.authorPubKey, identity.identifier, untitled) + } + val domain = + if (identity.authorPubKey == "browser") { + OmniboxInput.hostOf(identity.identifier) ?: identity.identifier + } else { + identity.identifier.ifBlank { identity.authorPubKey.take(12) + "…" } + } return NappletConnectInfo(appletTitle = title, coordinate = identity.coordinate, domain = domain, iconUrl = iconUrl) } 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 7e52e03f9d..a12e7b3638 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 @@ -57,6 +57,7 @@ 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.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -69,6 +70,8 @@ import com.vitorpamplona.amethyst.commons.napplet.signers.AppSignerPolicy import com.vitorpamplona.amethyst.commons.napplet.signers.NostrOpDecision import com.vitorpamplona.amethyst.commons.napplet.signers.NostrSignerOp import com.vitorpamplona.amethyst.commons.napplet.signers.NostrSignerPermissionLedger +import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry +import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel import com.vitorpamplona.amethyst.napplet.descriptionRes import com.vitorpamplona.amethyst.napplet.labelRes import com.vitorpamplona.amethyst.napplet.resolveNappletMeta @@ -238,14 +241,17 @@ private fun AppIdentityHeader(state: ConnectedAppDetailState) { horizontalArrangement = Arrangement.spacedBy(12.dp), ) { val isBrowserEntry = state.coordinate.startsWith("browser:") + val browserUrl = if (isBrowserEntry) state.coordinate.substringAfter(':') else null + val iconModel = if (browserUrl != null) rememberWebAppIconModel(browserUrl) else null val appForIcon = if (isBrowserEntry) { - FavoriteApp.WebApp(state.coordinate.substringAfter(':'), state.title, 0L) + FavoriteApp.WebApp(browserUrl ?: "", state.title, 0L) } else { FavoriteApp.NostrApp(state.coordinate, state.title, 0L, state.iconUrl) } FavoriteAppIcon( app = appForIcon, + iconModel = iconModel, tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.size(48.dp), ) @@ -449,7 +455,13 @@ private suspend fun loadDetailState( val signerPolicy = signerLedger.store.loadPolicy(coordinate) val opOverrides = signerLedger.store.allOpDecisions(coordinate) - val (title, iconUrl) = resolveNappletMeta(author, identifier, untitled) + val (title, iconUrl) = + if (author == "browser") { + val host = OmniboxInput.hostOf(identifier) ?: identifier + host to BrowserIconRegistry.iconModelFor(host) + } else { + resolveNappletMeta(author, identifier, untitled) + } return ConnectedAppDetailState( title = title, coordinate = coordinate, 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 8e7f41ca9b..c33f032923 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 @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionL import com.vitorpamplona.amethyst.commons.napplet.signers.AppSignerPolicy import com.vitorpamplona.amethyst.commons.napplet.signers.NostrSignerPermissionLedger import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel +import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -207,9 +208,11 @@ private fun BrowserAppCard( .ifBlank { url } } + val iconModel = rememberWebAppIconModel(url) + ConnectedAppCardLayout( app = FavoriteApp.WebApp(url, domain, 0L), - iconModel = null, + iconModel = iconModel, title = domain, subtitle = url, npub = null,