The find-or-create wizard can surface a wallet the user previously
DELETED — a relay that missed the kind:5 still serves the kind:17375 to
the crawl. adoptDiscoveredWallet rebroadcast that event verbatim (same id,
same created_at), which loses to the prior NIP-09 deletion two ways:
DeletionEvent.build emits both an `e` tag (old id) and an `a` tag (the
replaceable 17375:pubkey: address), so relays reject the duplicate id and
re-delete every version with created_at <= the deletion's the moment the
kind:5 propagates back — on relays and in our own LocalCache. The
"reactivated" wallet would then silently vanish.
Adopt now re-signs a FRESH kind:17375 + kind:10019 (via publishWalletEvents)
with the discovered wallet's own mints and P2PK key. A new id escapes the
`e`-tag delete and created_at=now escapes the `a`-tag delete, while the
same key + mints preserve the nutzap address and all recoverable funds
(the NUT-13 seed derives from the key, not the event id). Falls back to a
verbatim rebroadcast only if the wallet can't be decrypted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
Deleting the Cashu wallet popped back to CashuWalletScreen, which on an
empty wallet auto-launches the find-or-create wizard — so the user was
funneled straight back into creating the wallet they just deleted.
Navigate to the top-level Wallet hub (Route.Wallet) via newStack instead,
which pops the Cashu screens off the back stack so the wizard never
composes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
In the find-or-create wizard, choosing "Create a new wallet" jumped
straight to the mint manager. Add a short celebratory interstitial first:
an animated check badge springs in (bouncy overshoot) behind an expanding
pulse ring, "Wallet Created" + "Now pick a few mints to host your sats"
fade up, a haptic fires, and a "Pick mints" button continues to the mint
selection.
The screen is purely presentational — the kind:17375 still isn't
published until the user adds a mint on the next screen — so "Pick mints"
uses popUpTo to replace the interstitial in the back stack, avoiding an
awkward return to the celebration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
The mint picker (CashuMintsScreen) already renders NIP-87 directory
suggestions, but in the find-or-create wizard's "create a new wallet"
path they showed up empty while the edit-mints screen had them.
Cause: openMintDirectory() subscribed the directory against a one-shot
snapshot of the account's outbox relays (acc.outboxRelays.flow.value). A
freshly-restored account reaching the create path often still has its
relay lists loading, so the snapshot was empty, hit
CashuMintDirectoryState's empty-relay early-out, and never retried.
Outbox-only relays also don't reliably carry the broad NIP-87 mint
announcements.
Fix: subscribe reactively to the union of the user's OUTBOX relays and
their INDEXER relays. The flow re-subscribes the moment relays arrive,
and indexer relays aggregate NIP-87 mint data and fall back to a curated
default set — so the picker is populated even for a brand-new user with
no wallet and no recommendations of their own yet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
Merges nostr proposal 81ee3e7c into main:
- fix(napplet): drop algorithmic darkening so dark-by-default sites (e.g.
ditto.pub) aren't corrupted — it collided with nightThemedContext and inverted
their nav bars to light. prefers-color-scheme: dark still works without it.
- feat(embed): follow the app theme live in embedded web tabs by rebuilding the
warm sessions on a DARK/LIGHT flip, instead of requiring an app restart.
Device-verified on ditto.pub + brainstorm.nosfabrica.com, Dark<->Light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An embedded tab's WebView resolves its theme from the context it is built with
(nightThemedContext), once, at construction — a runtime config change does not
re-flip the renderer — so a live app-theme switch never reached an already-warm
surface; it took a full app restart.
Watch the resolved DARK/LIGHT theme and, on a real flip, rebuild the warm
sessions in the new theme:
- EmbeddedTabHost.themeEpoch + rebuildAllForTheme() tears down the warm
controllers but keeps activeId, so the visible tab re-activates the instant its
screen re-acquires (no blanked-out surface).
- EmbeddedTabThemeWatcher (mounted by AppNavigation next to the tab layer)
collects uiPrefs.theme + isSystemInDarkTheme() and triggers the rebuild.
- WebAppScreen / NostrAppScreen key their controller on themeEpoch (NostrApp also
re-mints its launch params); the preloader re-warms off-screen tabs; the tab
layer keys each surface on (id, controller) so a rebuilt session gets a fresh
SandboxedSdkView.
The page reloads in the new theme (unavoidable — the theme is fixed at WebView
construction). Device-verified Dark<->Light with no app restart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setAlgorithmicDarkeningAllowed(true) was added to force-darken pages that don't
implement prefers-color-scheme. Combined with nightThemedContext (which forces the
embed WebView's isLightTheme=false), it now also runs on pages that are ALREADY
dark but don't declare CSS color-scheme support — e.g. ditto.pub, which ships
<html class="dark"> by default — and algorithmically inverts their nav bars to
light, leaving "dark content, light bars".
prefers-color-scheme: dark is driven by isLightTheme (nightThemedContext)
INDEPENDENTLY of algorithmic darkening — device-verified: embedded pages still
report prefersDark=true with darkening off — so dropping it keeps real dark-aware
sites dark while no longer corrupting dark-by-default ones. The trade-off (a site
with no dark mode of its own renders light instead of being force-inverted)
matches how a real mobile browser behaves.
Removed from all four embed/host WebView configs (browser + nsite/napplet,
embedded + full-screen) along with the now-unused WebSettingsCompat import.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merges nostr proposal 6650c40b into main:
- Square blurhash placeholder: render with ContentScale.Crop when the imeta dim
is known, so it fills the correctly-sized box instead of letterboxing a
component-grid-square bitmap inside a taller portrait box.
- "Can't play this video" flash: never return a warm-pooled ExoPlayer carrying a
stale PlaybackException — releasePlayer drops one that errored before pooling,
acquirePlayer drops one whose decoder died asynchronously while warm.
- Adds playback-error lifecycle logging used to trace both issues from logcat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two inline-video bugs surfaced by a portrait Damus post (imeta dim 720x1280, a
rotated H.264 file) that showed a square blurhash in a too-tall box and flashed
"Can't play this video":
- Square blurhash placeholder: the placeholder bitmap is decoded at the
blurhash's DCT component-grid aspect (e.g. a 5x5 grid -> a square bitmap), not
the real media shape. When the true ratio is known (from imeta dim) the box is
already sized correctly, so render the placeholder with ContentScale.Crop to
fill it instead of letting FillWidth letterbox a square inside the taller
portrait box.
- "Can't play this video" flash: a warm-pooled ExoPlayer could be handed back
still carrying a stale PlaybackException. releasePlayer now drops a player that
errored before being pooled; acquirePlayer drops one whose decoder died
asynchronously while it sat warm (surface reclaim / codec loss). Either way a
clean cold/fresh player is used and the stale error never reaches a controller.
Also adds playback-error lifecycle logging (PlaybackError tag with a flattened
cause chain, a live-controller counter, and cold-load + acquire-time
stale-error markers) that made both issues traceable from logcat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The top bar on the Search screen (search field + filter row with the 3
scope buttons) had no background, so the feed scrolled visibly behind
the segmented buttons and the gaps in the bar.
Apply the theme surface color to the SearchBar Column, before
statusBarsPadding() so the status-bar inset is filled too, matching the
default Material3 top-app-bar container color used elsewhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkfRpnNyeeo3AVEaX71oRX
isLight fans out to hundreds of themed-color getters on hot note/chat/feed
render paths. The accent-color work had switched it to background.luminance(),
which adds per-call gamma math. Since the accent never touches background
(only primary/secondary) and the dark palette's background is exactly
Color.Black, a single reference comparison is just as accent-robust and
restores the original constant-time cost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REGsru6cnm6wUzqm12Rh2d
The gallery style selector is profile-specific, so it now lives on the
Profile UI settings screen alongside the other profile display toggles
instead of the general Application Preferences screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REGsru6cnm6wUzqm12Rh2d
Adds three new appearance settings to Application Preferences, alongside
the existing Theme selector:
- Accent Color: Purple (default), Blue, Green, Orange, Red, Pink. Drives
the Material primary/secondary/tertiary colors so buttons, links, FABs
and switches follow the chosen hue. Purple preserves the original look
(purple primary + teal secondary).
- Font: System Default, Sans Serif, Serif, Monospace. Applied to the full
Material typography and to bare Text via LocalTextStyle.
- Font Size: Small, Normal (default), Large, Huge. Scales all text through
LocalDensity.fontScale without affecting dp-based layout.
Plumbed through the existing UiSettings -> UiSettingsFlow ->
UiSharedPreferences (DataStore) pipeline and the AmethystTheme composable.
New fields default to the current behavior and are appended, so existing
stored settings deserialize unchanged.
ColorScheme.isLight now derives from background luminance instead of a
fixed primary, so the light/dark check keeps working when a non-purple
accent is selected. The primary-derived tint extensions (links, new-item
background, secondary button) now compute from the live scheme so they
track the accent color.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REGsru6cnm6wUzqm12Rh2d
The napplet/nsite host (NappletHostActivity) removed its loading splash the
moment the index probe succeeded and then mounted a WebView with no progress
tracking at all, so during the seconds the shell + bundle take to load (notably
over Tor) the user saw only the WebView's dark colorBackground — a black screen
with no sign anything was happening, especially in dark theme.
- Add a thin browser-style determinate progress bar pinned to the top edge,
driven by WebChromeClient.onProgressChanged and hidden at 100%, to both the
napplet/nsite host and the URL browser (NappletBrowserActivity).
- Mount the WebView under the loading splash and keep the splash (now opaque)
until first paint (onPageCommitVisible) instead of removing it on mount, so
there is never a blank/dark gap between probe-success and the shell's first
frame. This mirrors the pattern the URL browser already used.
- Add a developer console (NappletConsolePanel) to the napplet/nsite host,
wired through the existing onConsole hook in NappletControlSheet, and forward
the page's console.* output to it.
- Surface failed resource fetches (onReceivedError / onReceivedHttpError) as
ERROR lines in the console on both hosts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D4iYA4Qf5guWZyKexkcmhb
Add two Discover sections to the browser launcher home that surface the
NIP-5A sites and NIP-5D apps published by the people the user follows,
reusing the same feed + follow-list filter as the dedicated nSites and
nApplets screens (set those to All Follows for a pure follows list).
The launcher subscribes the nsite/napplet assemblers while open, observes
the addressable manifest store, keeps the followed authors' (or own, in
the Mine case), drops ones already pinned, and caps each section. Rows
mirror the web Discover row — manifest icon, title, description, and a
star to pin — launching Nostr-natively through the sandboxed host.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Tapping the "loads over Tor" row on an nSite's pull-down sheet popped a
confirm dialog explaining the routing change. Users already know what Tor
is, so toggle the routing directly on tap (still relaunches the session to
rebuild the proxy + content server) and remove the now-unused strings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177rhf2L93YRcq6NrWkkM4Q
The omnibox now ranks the hardcoded Discover web apps alongside
favorites and history, so typing finds a suggested app even before its
first visit (the ranker dedupes by host, and favorites/history outscore
a plain default, so an already-pinned/visited app never doubles up).
Search results are split into Favorites / Recent / Discover groups using
the visited-URL set so the headers stay accurate, and every result row
now carries the same 3-dot menu as the idle Recent cards: pin/unpin to
favorites, plus remove-from-history for visited sites. The favorite
toggle is shared with the Recent rows via one helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
The nutzap gallery in MultiSetCompose used WidthAuthorPictureModifier
(55dp, flush-right) for its cashu icon column, while the like/boost
reaction galleries above it use NotificationIconModifier (55dp with a
5dp end inset). That left the cashu glyph sitting ~5dp further right
than the reactions it stacks under. Switch the cashu Box to the same
NotificationIconModifier so the icons line up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qQdaD3DHRQNDKe2BUnEiM
Render the browser "Discover" section as full-width rows (icon + name +
one-line description) instead of bare icon cells, matching the Recent
row layout which already carries a subtitle. Each suggestion now has a
short curated description (trimmed from the app's own meta description)
so unfamiliar apps explain themselves; tapping a row opens the app, and
a trailing star pins it to favorites.
Auto-pulling page <title>/description was rejected: many of these apps
are client-rendered SPAs that serve an empty <title>, and several titles
are long marketing strings — curated short names read better in the list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Grow the browser "Discover web apps" list to the full set of
browser-openable Nostr web apps from the nostrapps.com directory plus
several requested additions, and give each entry its own logo.
- Each suggestion now carries iconUrl set to the app's own declared
apple-touch-icon / icon (PNG or SVG, individually verified to return an
image), so the grid matches the favicon look of Favorites/Recent
without any third-party favicon service. Apps whose only icon is an ICO
(Coil has no ICO decoder) or that couldn't be resolved stay icon-less
and fall back to the globe glyph until their favicon is captured on
first visit.
- Added: nymchat, nostr.build, nostrcheck, zap.cooking, x21, divine.video,
brainstorm, zappix, plektos, zaptrax, zaplytics, podstr, ghostr, mutable,
metadata, plebsvszombies, blobbi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Add a "Discover web apps" section to the browser launcher home, shown
after the Recent block, with a hardcoded list of popular Nostr web apps
drawn from the nostrapps.com directory. Gives new users (whose Favorites
and Recent are empty) somewhere to start instead of a bare empty screen.
- New DefaultWebClients in commons (URL + label entries), grouped by
category; extensions/signer-only tools are excluded and every URL is a
confirmed canonical domain. No remote icons are loaded on the idle
screen — favicons are captured the normal way once a site is opened.
- Render the list via a new suggestedAppItems grid (long-press offers
"Add to favorites"); already-favorited apps are filtered out.
- FavoriteAppCell now takes a menu slot so the favorites grid and the
suggestions grid can offer different long-press actions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Add a new top-level feed for NIP-34 Git repository announcements
(kind 30617), mirroring the Pictures/Videos/Workouts feeds.
- GitRepositoriesFeedFilter scans LocalCache addressables for kind 30617
- Full top-nav filter support (follows, authors, global, hashtag,
geohash, communities, muted) via a per-relay sub-assembler set
- Wired into AccountFeedContentStates, the relay subscription
coordinator, bottom-bar preloaders, navigation, drawer and the
persisted per-feed follow-list setting
- Reuses the shared RenderGitRepositoryEvent card via the standard feed
render path, enriched with topic chips and a personal-fork badge
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GS371vPHy3PhMfQyeAHmZC
Adds androidx.compose.runtime:runtime-tracing and tracing-perfetto so
recompositions show up as named slices in Perfetto system traces — the
tool used to attribute the cold-start feed first-paint cost to specific
composables. debugImplementation only (not shipped); all Apache-2.0.
Usage (runtime-enable broadcast) documented inline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The debug-only MemoryUsageChip ("X/YMB" top-bar indicator, gated on
isDebug) polls collectMemorySnapshot() every 2s from a produceState
block, which runs on the main thread. That reads coil3.disk.DiskLruCache
.size(), a @Synchronized call. On cold start the Coil disk cache holds
that monitor for several seconds (journal init + the burst of image
writes from the initial relay event flood), so the UI thread blocked
inside size() — the "Loading account" frame couldn't repaint until it
returned. Profiling showed a single ~8s render frame and the UI thread
"blocking from coil3.disk.DiskLruCache.size()".
Collect the snapshot via withContext(Dispatchers.IO) so the synchronized
read blocks a background thread instead of the UI. The "Loading account"
stall on cold start drops from ~15-20s to ~5s. Debug-only path, so this
never affected release builds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Some codec failures never surface as a PlaybackException: a software HEVC
decoder that can't keep up (e.g. iPhone-recorded hvc1 video on a device
without a HEVC hardware decoder) just parks the player in STATE_BUFFERING
forever — the buffer fills to the LoadControl cap, the playhead never leaves
0, and no error is ever raised. WatchPlaybackErrors only listened for
onPlayerErrorChanged, so the existing RenderPlaybackError "Open in browser"
overlay never showed and the user stared at a blank buffering box.
Add a decode-stall watchdog that polls the controller and synthesizes a
PlaybackException (ERROR_CODE_DECODING_FORMAT_UNSUPPORTED) once the player
sits in STATE_BUFFERING, wanting to play, with >=2s of media buffered ahead
(decoder is fed, not network-starved) yet a frozen playhead for 8s. The
buffer-ahead guard distinguishes a hung decoder from genuine network
starvation, whose buffer is depleted and so is never flagged.
Recovery is automatic: the overlay clears on the STATE_READY transition and,
belt-and-suspenders, the watchdog drops it the instant the playhead advances
again — so a slow device that eventually decodes "just plays." Also narrow
the recovery-clear to STATE_READY only (clearing on STATE_BUFFERING would
wipe the synthetic error instantly) and clear on onMediaItemTransition so a
pooled player starting a new video resets cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>