Replace the static placeholder glyph with the napplet manifest's
icon() URL loaded via FavoriteAppIcon (Coil AsyncImage + glyph
fallback). Both ConnectedAppsScreen and ConnectedAppDetailScreen now
resolve iconUrl alongside title from the event cache and display the
real app icon when available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
- Add AllowForSession and AllowUntil(expiresAt) signer grant types so users
can grant temporary access (session, 24h, 30d) from the consent dialog
- Track per-app lastUsed timestamp in NostrSignerPermissionStore and update
it on every granted signing operation
- Auto-expire timed grants: decide() clears expired op decisions before
returning, so no background sweep is needed
- Add NappletBroker.sessionAllows in-memory set for session grants (cleared
on broker destroy, never persisted)
- Implement full NIP-42 relay auth policy system:
- RelayAuthPolicy enum (ALWAYS / NEVER / IF_IN_MY_LIST) stored in
AccountSettings and persisted in LocalPreferences
- RelayAuthDecision (ALLOW / DENY) per-relay overrides in DataStore
- RelayAuthPermissionLedger combining global policy + per-relay overrides
- DataStoreRelayAuthPermissionStore writing to relay_auth.preferences_pb
- Wire relay auth into AuthCoordinator: subscribeLedger/unsubscribeLedger
lets each logged-in account contribute its own policy; signWithAllLoggedInUsers
now receives the relay URL so it can check the ledger before signing
- Update RelayAuthenticator (quartz) to pass relay URL in the signing lambda
- Add RelayAuthSubscription composable that subscribes both the account and
its ledger when a screen is active
- Add RelayAuthSettingsScreen: global policy radio picker + per-relay
override list with toggle and remove; reachable from Settings
- Add Route.RelayAuthSettings, AppNavigation wiring, and SettingsCatalog entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
Adds a unified "Connected Apps" entry to Account Settings that surfaces
all web apps that have connected to the user's Nostr key (both capability
grants and signer trust levels) in one place.
- Route.ConnectedApps (list) + Route.ConnectedAppDetail(coordinate) (detail)
- ConnectedAppsScreen: merges NappletPermissionLedger + NostrSignerPermissionLedger,
shows one card per app sorted alphabetically with trust-level chip
- ConnectedAppDetailScreen: identity header, editable trust-level picker
(Full Trust / Reasonable / Paranoid), op override rows with revoke, capability
switches, and a "Forget this app" button that clears all permissions
- Settings catalog entry under Account (Apps symbol, keyword-searchable)
- NappletsTopBar Tune icon now navigates to ConnectedApps instead of
the old capability-only NappletPermissions screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
buildEventJson was manually escaping quotes, newlines, and backslashes
which is fragile. org.json.JSONObject.toString(2) handles all escaping
correctly and produces the same pretty-printed output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
- NappletSignerConsentActivity: replace AlertDialog (broken 5-button
layout) with a custom Dialog + Surface using heightIn + verticalScroll;
color-coded allow (primary) / deny (error) action rows; monospace
"See more" toggle that reveals full raw event JSON with SelectionContainer
so users can inspect and copy the data being signed/encrypted
- NappletConnectActivity: wrap column in verticalScroll so the trust-level
options are not clipped on small screens or large font sizes
- NappletSignerPermissionsScreen: show localized op labels ("sign kind 1
event") and decision labels ("Allow"/"Ask"/"Deny") instead of raw key
strings and enum names; fix per-op delete touch target to 48dp (M3 min)
- NappletSignerConsentInfo: add rawData field carrying full event JSON for
sign/encrypt or decrypted plaintext for future decrypt operations
- NostrSignerOpLabels: populate rawData; add buildEventJson helper
- strings: napplet_op_decrypt → "read your private messages" (the consent
is to expose already-decrypted content, not to perform decryption);
add napplet_consent_wants_to, see_more/see_less, decision labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
Implements per-app permission management for the internal nsec signer when
webapps/napplets/nsites connect via Amethyst's built-in key:
- Three trust levels on first connect (FULL_TRUST, REASONABLE, PARANOID)
with a UI dialog (NappletConnectActivity) matching the design spec
- Per-operation consent dialogs (NappletSignerConsentActivity) for
sign-kind/encrypt/decrypt with Allow once, Don't ask again, Deny options
- Per-app DataStore storage (DataStoreNostrSignerPermissionStore) using
SHA-256-hashed filenames so 1000s of apps don't bloat a single file
- NostrSignerPermissionLedger applies policy decisions: REASONABLE
auto-allows kinds 1/6/7; FULL_TRUST auto-allows all non-payment ops
- NappletBroker extended with first-connect gate and per-op signer gate,
serialized by a dedicated signerConsentLock mutex
- Permission management screen (NappletSignerPermissionsScreen) to review
and revoke stored per-app signer permissions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
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>