From bb968f74f2f6edd53c44213d45f1591eb415b7d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 18:23:52 +0000 Subject: [PATCH] fix: make nsite/napplet favorite icon reactive to manifest arrival MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first cut resolved the manifest once inside remember(coordinate), so if the event wasn't in LocalCache at first composition (e.g. a cold start, where it streams in from relays a moment later) the null result was cached for the life of the composition and the icon never appeared — a silent fall-back to the glyph. Observe the addressable note's metadata StateFlow instead, mirroring the webapp favicon path (which re-resolves on the BrowserIconRegistry key set): when the manifest lands or updates in LocalCache, the icon resolves and the blob fetch kicks off. checkGetOrCreateAddressableNote returns null only for a malformed coordinate, so the early return stays structurally stable per call site. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AaPdt8EnSrSBuzdudrxzTH --- .../amethyst/favorites/NappletFavoriteIcon.kt | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) 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 df1e6c1a76..b600e20057 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/NappletFavoriteIcon.kt @@ -27,10 +27,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.model.LocalCache import com.vitorpamplona.amethyst.napplethost.NappletBlobCache import com.vitorpamplona.amethyst.napplethost.NappletBlobPrefetcher +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag @@ -58,15 +60,26 @@ private class IconBlob( * iframe under the trusted shell, so `WebChromeClient.onReceivedIcon` only ever reports the shell's icon, * never the applet's. Deriving it from the bundled blobs is the only path that sees the real icon. * - * Returns null until the blob is on disk; the icon then appears on the next recomposition. All disk + network - * work runs off the composition thread. The blob is usually already cached (the browse/feed card prefetches - * every manifest blob, this one included); the on-demand fetch here just covers favorites whose card isn't - * currently on screen. + * Observes the addressable note, so the icon resolves whenever the manifest arrives or updates in + * LocalCache — not only if it already happened to be cached at first composition (on a cold start the + * event streams in from relays a moment later). Returns null until the blob is on disk; the icon then + * appears on the next recomposition. All disk + network work runs off the composition thread. The blob is + * usually already cached (the browse/feed card prefetches every manifest blob, this one included); the + * on-demand fetch here just covers favorites whose card isn't currently on screen. */ @Composable fun rememberNappletIconModel(coordinate: String): String? { val context = LocalContext.current - val icon = remember(coordinate) { resolveIconBlob(coordinate) } ?: return null + + // checkGetOrCreate returns null only for a malformed coordinate, so this early return is stable for a + // given coordinate (it never flips across recompositions, which would break composition structure). + val note = remember(coordinate) { LocalCache.checkGetOrCreateAddressableNote(coordinate) } ?: return null + val noteState by note + .flow() + .metadata.stateFlow + .collectAsStateWithLifecycle() + + val icon = remember(noteState) { resolveIconBlob(noteState.note.event) } ?: return null var model by remember(icon.path.hash) { mutableStateOf(null) } LaunchedEffect(icon.path.hash) { @@ -82,9 +95,9 @@ fun rememberNappletIconModel(coordinate: String): String? { return model } -/** Reads the live manifest from [LocalCache] and asks each event type for its bundled icon blob + servers. */ -private fun resolveIconBlob(coordinate: String): IconBlob? = - when (val event = LocalCache.getAddressableNoteIfExists(coordinate)?.event) { +/** Asks each nsite/napplet event type for its bundled icon blob + the servers that hold it. */ +private fun resolveIconBlob(event: Event?): IconBlob? = + when (event) { is RootNappletEvent -> event.iconBlob()?.let { IconBlob(it, event.servers()) } is NamedNappletEvent -> event.iconBlob()?.let { IconBlob(it, event.servers()) } is RootSiteEvent -> event.iconBlob()?.let { IconBlob(it, event.servers()) }