mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat: show website favicon and domain in all browser-connected app views
Web app entries (browser:https://...) now display: - The captured favicon from BrowserIconRegistry (same source as the bottom-nav favourite website icon) in ConnectedAppsScreen, ConnectedAppDetailScreen, and all three permission/consent dialogs - The domain name (host) as the title instead of the full URL, via OmniboxInput.hostOf() in loadDetailState, NappletConsentSummary, buildSignerConsentInfo, and buildConnectInfo - A globe icon (FavoriteApp.WebApp) instead of the grid icon (FavoriteApp.NostrApp) in all consent dialog headers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
This commit is contained in:
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
+7
-1
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+14
-2
@@ -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,
|
||||
|
||||
+4
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user