Commit Graph
15609 Commits
Author SHA1 Message Date
Claude 41843dd8e8 fix: stop bottom bar resetting on upgrade; make pull-tab a dismissable drawer
Two regressions from this session's audit batch:

1. Bottom nav bar reset. Adding stable @SerialName discriminators to
   BottomBarEntry changed the persisted polymorphic "type" value from the
   fully-qualified class name to "builtIn"/"favorite", so configs written by an
   earlier build no longer decoded — and the fallback returned an empty list,
   blanking the bar. decodeBottomBarItems now migrates the old fully-qualified
   discriminators to the short names (recovering the user's customized bar), and
   any unrecognizable value falls back to the defaults instead of empty. Locked
   with BottomBarEntrySerializationTest.

2. Pull-down sheet interfered with page taps. The expanded top sheet is a
   full-width drawer with no way to dismiss except the grabber, so it sat over
   the page. Hoist its expanded state into EmbeddedTabLayer (reset per tab) and
   draw a full-area dismiss scrim behind the open sheet; collapsed, only the
   small grabber is interactive and page taps pass through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 00:32:44 +00:00
Claude ba2af75b4c feat: replace full-screen sandbox corner chip with top pull-down sheet
The full-screen browser/napplet-host activities still showed the old corner
pill/globe — which sits exactly where a site puts its own login avatar. Add a
native-View NappletControlSheet (the twin of the embedded tabs' Compose
TopControlSheet): a small grabber at the top edge that pulls down to the page's
controls, and wire both :napplet full-screen activities to it.

- NappletControlSheet: title row (shield/globe), optional Tor row, reload, and
  optional "what it can access". Tor supports an inline toggle (browser) or a
  tap-through to a confirm dialog (nSite host, where switching rebuilds the
  session). Tap or vertical drag to expand/collapse.
- NappletBrowserActivity / NappletHostActivity: drop buildFloatingChip/chipGlyph
  for buildControlSheet(); attach at Gravity.TOP, full width.
- Add short napplet_net_tor_label / napplet_net_open_label strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 00:10:29 +00:00
Claude af9e8b885d refactor: apply embedded-tab audit fixes (durability, perf, leaks, cleanup)
Durability & correctness:
- FavoriteAppsRegistry: tombstone removals made before async hydration so a
  just-deleted favorite can't be resurrected by the disk merge.
- BottomBarEntry: stable @SerialName discriminators so persisted bottom-bar
  configs survive class renames/moves.
- EmbeddedTabHost: guard reportBounds/setActiveChrome by active id so a
  cross-fading outgoing screen can't clobber the incoming tab's bounds/chrome.
- EmbeddedNappletController: replay a parked-before-bound pause after session
  create so a never-shown applet doesn't come up running.
- NappletBrowserService: bind the broker once (no leaked binding on re-create),
  destroy a stale WebView before rebuilding, and reload only after the async
  proxy override actually applies. NappletHostService: same WebView-reuse guard.
- NappletBrokerService: cap concurrent foreground leases so a misbehaving
  sandbox can't pin Tor/relays with unbounded arbitrary keys.

Perf:
- Screens publish a remembered EmbeddedTabChrome; host short-circuits identical
  publishes so the tab layer isn't recomposed every frame.
- AppBottomBar resolves favorites via an id-indexed map, not a per-entry scan.
- TopControlSheet keyed on the active tab so its expand state resets per tab.

Cleanup:
- Delete dead BrowserHostActivity + EmbeddedBrowserSurface + AppControlPuck and
  their manifest entry; drop the unused `ready` surface state; null controller
  refs on unbind; refresh stale z-order/host docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 00:05:34 +00:00
Claude 89a32e078f fix: audit batch 1 — drop diagnostics, browser foreground heartbeat, Tor host key
- Remove the shipped scroll/zoom diagnostics from NappletBrowserService (per-touch
  MotionEvent log and per-page zoom log).
- NappletBrowserActivity now renews its foreground lease on a 30s heartbeat like
  NappletHostActivity, so the broker's 90s watchdog can't reap it (tearing down
  Tor/relays) while the browser is genuinely foreground.
- Persist the per-host Tor choice against the host actually displayed (webView.url),
  not the start URL, so an in-page navigation doesn't save the choice to the wrong site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 23:51:18 +00:00
Claude 7d5f68d115 Merge remote-tracking branch 'origin/main' into claude/webview-menu-custom-url-ikrgbz 2026-06-23 23:41:38 +00:00
Claude 9ebe16d7a1 feat: top pull-down control sheet for embedded tabs
Now that the surface is z-ordered below the Compose layer (alpha17), chrome can
finally draw over the page — so replace the slim top bar with a top pull-down
sheet, per request. Collapsed it's just a small grabber centered at the top edge
(out of the top-right corner, where sites put their own avatar/menu); pull it
down or tap to reveal the page's controls: route over Tor, reload, "what it can
access" (sandboxed napplets/nsites), and open full screen.

The active tab publishes its controls as EmbeddedTabChrome; EmbeddedTabLayer
draws the TopControlSheet over the active tab's bounds, after the surfaces so it
sits on top. Applies to both embedded web and napplet/nsite tabs.

The full-screen activities still carry the native corner chip — converting those
to the same pull-down is the next step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 23:38:10 +00:00
Claude 90150e72e4 fix: upgrade privacysandbox.ui to alpha17 (embedded drag/scroll input)
The embedded surface forwarding taps but cancelling drags was a known bug in
androidx.privacysandbox.ui alpha10: with the provider surface z-ordered above,
"the gesture is exclusively received by the provider window and not transferred
to the client window" (alpha13 release notes). alpha15 then "set the default
Z-ordering to below" and "added support for the UI provider to receive
MotionEvents in this mode after being received by the client window" — i.e. the
drag-input path we needed.

Bump alpha10 → alpha17 and adapt the changed API: openSession takes SessionData
instead of a windowInputToken IBinder, Session adds notifySessionRendered, and
the session-state listener became setEventListener(SandboxedSdkViewEventListener)
(ready now flips on onUiDisplayed). The direct-WebView browser is unaffected
(it doesn't use this library).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 23:16:02 +00:00
Claude 1531f3baad feat: direct-WebView browser activity (scroll, zoom, keyboard)
Confirmed on a real device: the streamed SurfaceControlViewHost surface forwards
taps but drops scroll/zoom/keyboard gestures — a hard limitation of
androidx.privacysandbox.ui on current Android. No tweak fixes it.

Add NappletBrowserActivity: a full-screen browser that hosts the WebView
*directly* in its own window in the keyless :napplet process, so scrolling,
pinch-zoom, and the soft keyboard (windowSoftInputMode=adjustResize) all work
natively. It carries over NappletBrowserService's per-origin NIP-07 bridge and
Tor proxy, plus NappletHostActivity's trusted chip, loading screen, and
foreground hold — so it stays just as keyless (page JS runs in :napplet, every
window.nostr call is brokered + consent-gated per origin in the main process).

Web favorites and URL launches now open this activity instead of the streamed
BrowserHostActivity. Per-host Tor choice persists via a new MSG_SET_WEB_TOR
broker message (the :napplet process relays it to WebUrlNetworkRegistry).

The embedded bottom-row web tab still uses the streamed surface (it must, to
live inside MainActivity) and so still can't scroll — that's a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 22:46:31 +00:00
Claude b8786091dd fix: no black flash when switching between embedded tabs
Parked (inactive) warm tabs were shrunk to 1dp off-screen, so activating one
resized its surface from 1dp to full size — forcing the SurfaceControlViewHost to
re-render at the new size, which flashed black for ~1s (page appears → black →
reappears) on every tab switch.

Keep parked tabs at the SAME size as the active tab and only shift them
off-screen, so bringing one back is a pure translation: no resize, no re-render,
no black flash. They stay full-size and warm while parked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 22:25:44 +00:00
Claude 41376bac71 fix: restore input on embedded surfaces; controls in a slim bar
orderProviderUiAboveClientUi(false) broke input entirely — with the streamed
surface ordered below the window, the SurfaceControlViewHost stops receiving
touch, so neither the WebView nor the Compose puck got clicks (and the touch
logs went silent). Revert it: the surface must stay z-ordered on top for input.

That makes floating chrome over an embedded surface impossible (the surface is
always above any Compose UI in the window), so move the control puck into a slim
top bar above the surface on all streamed surfaces (embedded web/nsite/napplet
tabs and the full-screen browser). No title — the app titles itself. The native
NappletHostActivity keeps its floating chip (direct WebView, no surface).

Keep the EmbeddedSurfaceTouchHolder scroll fix (claims the gesture from host-side
ancestors). Drop the can't-show Compose loading overlay and the dead chrome-in-
layer machinery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 22:01:11 +00:00
Claude c44a18c803 fix: embedded surface scrolling, occluded chrome, and black load flash
Diagnosed from on-device logs: touch crosses the boundary but every drag gets
ACTION_CANCEL — a host-side ancestor intercepts the scroll, and the cross-process
WebView can't requestDisallowInterceptTouchEvent itself. Wrap the surface in
EmbeddedSurfaceTouchHolder, which claims the gesture on touch-down so the page
scrolls.

The floating puck couldn't sit over an embedded surface because the surface is
z-ordered on top of the window — so it was either hidden (full-screen browser)
or pushed below an empty reserve band that read as a black bar (embedded tabs).
Order the provider UI below the window UI (orderProviderUiAboveClientUi(false);
input still flows via the SurfaceControlViewHost token) and render the chrome on
top: the active tab publishes its controls as EmbeddedTabChrome and EmbeddedTabLayer
draws the puck over the surface — no reserve band, no black bar.

Add a themed loading placeholder (controller.ready flips on session Active) over
the surface in the tab layer and the full-screen browser, so binding + first
paint shows the app background + a spinner instead of a black box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 21:47:54 +00:00
Claude cb8bbad38d feat: floating control puck for running app surfaces + remember Tor per web client
Replace the full-width top bar on every running app surface (embedded web/nsite/
napplet tabs and the full-screen browser + napplet activities) with a small
floating control puck. Apps already title themselves, so instead of repeating
the name we keep one always-visible trusted marker — the sandbox shield for
napplets/nsites (the anti-phishing affordance the page can't draw over), a globe
for the plain browser — that expands on tap to reveal the actions (Tor, reload,
pop-out, access sheet, close).

- New shared AppControlPuck composable backs the three Compose surfaces;
  NappletHostActivity gets the native-View equivalent.
- Embedded tabs inset their reserved surface bounds by the puck height
  (AppControlPuckReserve) so the warm surface, drawn over those bounds above the
  nav tree, doesn't cover the puck. Full-screen activities float it on top.
- EmbeddedTabTopBar is removed (no longer used).

Also remember the Tor on/off choice per web client: some sites' servers reject
Tor exits, so a user opting one out must have it stick. New WebUrlNetworkRegistry
(main process, keyed by host, device-local) mirrors NappletNetworkRegistry; the
browser reads it for the initial route and writes on toggle, in both the embedded
tab and the full-screen browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 21:29:15 +00:00
Claude 2115129a72 chore: add zoom diagnostic to embedded browser
Logs the WebView's current scale, density, and pixel widths on page finish, to
confirm whether the 400% zoom is a density/viewport mismatch from streaming the
WebView through SurfaceControlViewHost. Temporary, alongside the touch/session
diagnostics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 21:03:17 +00:00
Claude 0b5d7925fe chore: temporary diagnostics for embedded browser scrolling
Adds two temporary logs to pin down why the embedded browser doesn't scroll:
- provider side (NappletBrowserService): logs each MotionEvent that reaches the
  remote WebView, so we can see if touch crosses the SurfaceControlViewHost
  boundary at all (returns false, never consumes).
- client side (EmbeddedBrowserController): logs the SandboxedSdkView session
  state transitions (Idle/Loading/Active/Error).

To be reverted once the cause is confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 20:58:09 +00:00
Claude 17c6aba934 fix: theme the sandbox WebView background instead of flashing white
The embedded browser / nsite / napplet WebView runs in the keyless :napplet
process, which has no access to the main app's Compose theme, so before a page
painted it showed the WebView default white — jarring against Amethyst's (often
dark) background.

Pass the theme background color (MaterialTheme.colorScheme.background) across the
process boundary and apply it to the WebView, and paint the SandboxedSdkView
placeholder with it too so there's no white flash before the first frame. The
full-screen NappletHostActivity resolves the color locally from its themed
context. Covers the embedded browser tab, the full-screen browser activity, and
the embedded nsite/napplet tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 20:52:18 +00:00
Claude d99ae48847 feat: lease-expire sandbox foreground holds so a dead host can't pin the network
The cross-process foreground hold relied on the `:napplet` host delivering its
onPause "false" to release. If that process dies while foreground (a crash, an
OS kill) it never sends it, and the broker would hold the main process resumed —
Tor/relays/AUTH up — forever.

Turn the hold into a renewing lease. A resumed host re-reports foreground on a
30s heartbeat; the broker stamps each launch token's last-seen time and a
watchdog reaps any lease older than a 90s TTL, releasing its hold. A live app
keeps renewing so it's never wrongly dropped; a dead one stops renewing and is
reaped within the TTL, bounding any leak to one window instead of forever.

Only the cross-process napplet/nSite path needs this — BrowserHostActivity runs
in the main process, so if it dies the hold and every connection die with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 20:28:36 +00:00
Claude 62f0adaa76 feat: keep Tor/relays/AUTH up while a sandbox surface is foreground
Opening a full-screen napplet/nSite (`:napplet` process) or browser host
(main process) backgrounds MainActivity, stopping its ManageRelayServices /
ManageWebOkHttp collectors. The underlying WhileSubscribed flows then scale
everything down on their timers — Tor's port (~2s), the relay pool (~30s),
dropping the relay AUTH sessions with it — even though the user is still on a
Nostr surface that brokers NIP-07 + relays back through that very process.

Add a ref-counted SandboxForegroundHold (main process): while held it
subscribes to exactly the flows the resumed UI subscribes to, keeping Tor, the
relay pool, and AUTH up; it releases when the last surface leaves so normal
background scaling resumes. Main-process activities call it directly; the
`:napplet` host can't touch that lifecycle, so it signals foreground over a new
MSG_SET_FOREGROUND IPC and the broker holds on its behalf (token-set keyed,
released on unbind).

Switching between several open surfaces (old.onPause -> new.onResume, plus the
async IPC hop) would dip the count to 0 and back to 1. A short release linger
keeps the same collectors running straight through that dip, so swapping
between open napplets/nSites/browser apps holds the network perfectly steady.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 20:06:12 +00:00
Claude a26a7c9552 fix: constrain the Tor onion to 24dp (was rendering at intrinsic size)
The ic_tor vector painter rendered at its intrinsic size because the Icon had
no size modifier, so it was oversized in the embedded web tab and the
full-screen browser. Pin it to the standard 24dp to match the reload icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 19:42:31 +00:00
Claude baf5c542a0 feat: favorite tabs use the app's own icon and drop the bottom-bar label
Two bottom-nav polish items:
- No label on favorite tabs — they now match the built-in items, which are
  icon-only.
- Use the app's own icon. FavoriteApp gains an optional iconUrl (the
  nsite/napplet manifest icon, captured when you favorite from its card and
  persisted). A shared FavoriteAppIcon renders that icon, falling back to a
  type glyph (the napplet/nsite mark, or the globe for a plain URL) when
  there's no icon or it fails to load. Used in both the bottom bar and the
  Favorite Apps grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 19:34:50 +00:00
Claude 5c37f57a31 refactor: use the Tor onion for the Tor toggle, shield only for sandbox access
Per review, drop the shield-as-Tor (and the security sheet that hosted it):
Tor is now the app's standard ic_tor onion (TorToggleButton, lit when on Tor,
dimmed on the open web), matching how Tor appears on relays and in settings.
The shield is reserved for the nsite/napplet tab, where it genuinely means
"what it can access".

- New shared TorToggleButton (onion).
- EmbeddedTabTopBar takes a `leading` slot: the web tab puts the Tor onion
  there, the napplet/nsite tab puts its access shield.
- BrowserHostActivity (full-screen) uses the same onion toggle instead of the
  shield/sheet. WebAppSecurityDialog and its strings are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 19:24:24 +00:00
Claude 2d3dd21695 fix: crash measuring embedded sandbox WebView (LayoutParams cast)
The surface adapters set the WebView's layoutParams to a plain
ViewGroup.LayoutParams, but the SurfaceControlViewHost container measures its
children with measureChildWithMargins, which casts to MarginLayoutParams —
crashing the :napplet process with a ClassCastException on first layout.

Use FrameLayout.LayoutParams (a MarginLayoutParams) in both
NappletHostUiAdapter and NappletBrowserUiAdapter (openSession + notifyResized).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 19:19:41 +00:00
Claude 1c60760c06 refactor: full-screen browser uses the same shield = security/privacy sheet
The full-screen BrowserHostActivity still used the shield (Security icon) as a
bare Tor toggle (pink when on Tor, plain when on the open web) — the same
icon-overload we removed from the embedded tabs. Switch it to the shared
"Security & privacy" pattern: the shield opens the WebAppSecurityDialog with
the Tor toggle inside, matching FavoriteWebAppScreen. The page keeps its
back/close + reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 19:14:28 +00:00
Claude b3e5e9e693 fix: inset the browser launcher omnibox below the status bar
The launcher's address field is a plain Row in the Scaffold topBar slot, so
(unlike a Material3 TopAppBar) it didn't apply the status-bar inset and drew
under the status bar. Add statusBarsPadding() to the omnibox row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 18:59:30 +00:00
Claude 77e41ddc00 refactor: one consistent top bar for embedded web/nsite/napplet tabs
The web-app tab and the nsite/napplet tab had different bars, and both used
the same MaterialSymbols.Security "shield" for different things — Tor on the
web tab, sandbox access on the napplet tab — which read as the same icon
meaning two things. (Tor's real mark is the ic_tor onion used elsewhere.)

- Add a shared EmbeddedTabTopBar (sandbox shield · title · reload · pop-out)
  used by both tabs, so they're visually identical.
- The shield now consistently means "security & privacy": it opens a sheet.
  Tor moves into that sheet (a live toggle on the web-app tab; the napplet
  sheet keeps its capability list + network line), so the shield is never
  confused with a Tor toggle and there's no standalone Tor icon to mistake
  for it.

(The full-screen BrowserHostActivity pop-out still uses its own bar; can
harmonize that next if wanted.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 18:11:17 +00:00
David KasparandGitHub 2311a4cd1b Merge pull request #3345 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-23 19:06:44 +01:00
Claude 601ba0a942 fix: stop embedded sandbox WebViews from double-applying system-bar insets
The full-screen NappletHostActivity already consumes system-bar + cutout
insets at its root so its WebView doesn't pad the page for the status/nav
bars a second time (targetSdk 35+ auto-applies received insets to web
content). The embedded surfaces — the browser tab and the new embedded
nsite/napplet tabs — have no such root: the WebView is the surface view, so
it received and re-applied those insets, leaving an empty band under the
status and navigation bars even though the host already places the surface
in the inset-free content area.

Add WebView.dropSystemBarInsets() (zeroes system-bar + display-cutout
insets, keeps IME for keyboard resize) and apply it in both
NappletBrowserService and NappletHostService when the session WebView is
built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 17:59:05 +00:00
Claude 25b530559e Merge remote-tracking branch 'origin/main' into claude/webview-menu-custom-url-ikrgbz 2026-06-23 17:38:09 +00:00
Claude 6b0bb9e859 refactor: unify the bottom bar into one ordered list of built-ins + favorites
Replaces the parallel bottomBarItems (List<NavBarItem>) + bottomBarFavoriteIds
with a single ordered List<BottomBarEntry>, so built-in destinations and
favorite apps live in one list and can be pinned and drag-reordered together.

- BottomBarEntry = BuiltIn(NavBarItem) | Favorite(favoriteId). The favorite id
  already encodes the route's parameters (the url / addressable coordinate), so
  each entry maps deterministically to its Route — BuiltIn via NavBarCatalog,
  Favorite via Route.FavoriteWebApp/FavoriteNostrApp. (Storing the raw Route
  isn't an option: the sealed Route parent isn't @Serializable, so a List<Route>
  can't be persisted without annotating the whole ~100-subtype hierarchy.)
- UiSettings/UiSettingsFlow carry bottomBarItems: List<BottomBarEntry>;
  UISharedPreferences serializes it as JSON, with a legacy comma-separated
  NavBarItem fallback so existing configs still load.
- BottomBarSettingsScreen now shows one reorderable list mixing built-ins and
  favorites; the separate favorites section is gone.
- AppBottomBar renders entries in saved order; warm-keep membership derives
  from the list's favorite entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 17:21:52 +00:00
vitorpamplonaandgithub-actions[bot] 8ffc65d727 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-23 17:00:04 +00:00
Vitor PamplonaandGitHub ff2c6f6313 Merge pull request #3344 from davotoula/fix/markdown-detect-heading-after-blank-line
fix: render markdown when a heading/list follows a blank line
2026-06-23 12:58:14 -04:00
davotoulaandClaude Opus 4.8 a97529d7af fix: detect markdown when a heading/list follows a blank line
isMarkdown() treated the second newline of a blank line as a non-space
character, flipping the line-start tracker off. ATX headings, blockquotes,
and list markers that follow the standard blank-line spacing went
undetected, so NIP-23 long-form articles made of prose plus section
headings rendered as raw text. Exclude newline/carriage-return from the
line-start guard so a blank line stays at line start.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:23:22 +02:00
David KasparandGitHub 7d77a057d6 Merge pull request #3343 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-23 17:16:18 +01:00
davotoulaandgithub-actions[bot] dd062861f3 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-23 16:14:26 +00:00
David KasparandGitHub 9c1392a2e0 Merge pull request #3341 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-23 17:12:37 +01:00
vitorpamplonaandgithub-actions[bot] 832951b90c chore: sync Crowdin translations and seed translator npub placeholders 2026-06-23 15:58:03 +00:00
Vitor Pamplona b7cbc03b48 Fixes the compilation error 2026-06-23 11:55:40 -04:00
Vitor Pamplona bf10498caf Merged 2026-06-23 11:50:36 -04:00
Claude e4f2c5305d feat: keep pinned embedded tabs warm via a persistent surface layer
Implements keep-warm (approach A) so a bottom-row embedded tab keeps its
full state across tab swaps, scoped to bottom-row apps per review.

Why a persistent layer: androidx.privacysandbox.ui's SandboxedSdkView
closes its session in onDetachedFromWindow, so a session torn down the
moment a tab leaves composition is unavoidable if the surface lives inside
the per-screen composable. Instead, a single app-shell overlay
(EmbeddedTabLayer) holds every warm session's SandboxedSdkView attached the
whole time — the active one positioned over the current tab's reserved
content area, the rest parked off-screen but alive. No provider changes
needed: the WebView never detaches, so its JS state survives.

- EmbeddedTabHost: process-level holder of warm sessions (keyed by
  FavoriteApp.id), the active id, and the active content bounds.
- EmbeddedSurfaceController unifies the browser + napplet controllers so the
  layer can attach/park/teardown either; the napplet controller pauses its
  applet while parked (onHidden) and on app-background.
- FavoriteWebAppScreen / FavoriteNappletScreen no longer host the surface;
  they reserve the content area (reporting window bounds) and drive the warm
  controller. The trusted napplet chrome stays in the main process.

Scope (per review): only bottom-bar favorites stay warm — a tab whose app
isn't a bottom-bar favorite is evicted (restarted) when it leaves
(EmbeddedTabHost.retainOnly, driven by the settings list). Genuine memory
pressure (onTrimMemory) drops all warm sessions; mere backgrounding does not.

Needs on-device verification of the surface overlay (alignment, touch
pass-through to the bars, warm re-show) — validated here by build + assemble.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 15:32:15 +00:00
Claude b373a297b6 refactor: configure favorite-app bottom-bar tabs from the settings page
Reworks how favorites get into the bottom bar, per review: instead of a
grid Pin/Unpin that auto-appended to the bar, favorites are now activated
as a dedicated "Favorite apps" section in the bottom-bar settings page —
kept separate from the built-in destinations because favorites are dynamic
data, not the fixed NavBarItem enum.

- UiSettings/UiSettingsFlow/UISharedPreferences gain bottomBarFavoriteIds
  (a device-local list of FavoriteApp ids), persisted alongside the
  existing bottomBarItems.
- BottomBarSettingsScreen gets a "Favorite apps" section: one toggle per
  favorite to activate/deactivate it as a bottom-bar tab.
- AppBottomBar renders the favorite tabs from that settings list instead of
  a registry-side pinned set.
- FavoriteAppsRegistry drops the pinned-set machinery; the grid drops its
  Pin/Unpin item. The "All apps" grid remains a built-in destination.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 15:22:52 +00:00
Vitor Pamplona 1eca812aec Fixes tests 2026-06-23 11:08:24 -04:00
Vitor Pamplona 1365be12a5 Fixes tests 2026-06-23 11:05:04 -04:00
Claude e82f1057c8 feat: embed nsites/napplets as in-process tabs; pin them to the bottom bar
Completes the favorites system: a favorited nsite/napplet can now be
pinned to the bottom bar and render as an embedded, swap-in-place tab —
no longer only a full-screen activity launch.

New sandbox surface (:napplet, keyless), mirroring the browser embed:
- NappletHostService hosts the verified-blob WebView (same content server,
  shell bridge, and single launch-token broker path as NappletHostActivity)
  and ships it as a SandboxedUiAdapter surface, so applet JS still runs only
  in the keyless process — never where the keys live.
- NappletHostUiAdapter / NappletEmbedContract are the SurfaceControlViewHost
  adapter and the Messenger contract; the create-session bundle reuses
  NappletHostContract's EXTRA_* keys, so the embedded and full-screen host
  paths launch from identical, main-process-minted parameters.

Main process:
- NappletLauncher.buildLaunchParams extracts the verified param/token minting
  so both the activity intent and the embedded session share it.
- EmbeddedNappletController binds the service and attaches the surface
  (mirror of EmbeddedBrowserController).
- FavoriteNappletScreen draws the TRUSTED CHROME (sandbox shield, app name,
  "what it can access") in the main process around the surface — the sandbox
  must never draw chrome the user is meant to trust — plus a pop-out to the
  full-screen host. Capability consent still flows through the existing
  main-process broker + consent activity, unchanged and host-agnostic.

Security parity with the full-screen host:
- The applet's JS + timers are paused while the app is backgrounded
  (lifecycle ON_STOP/ON_START → MSG_PAUSE/MSG_RESUME), so an "allow always"
  napplet can't act on the user's behalf when they aren't looking.
- Granted sensitive ops (publish/upload/pay) surface a notice toast.

Both favorite kinds are now pinnable; the bottom bar routes WebUrl →
FavoriteWebApp and NostrApp → FavoriteNostrApp, each embedding in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 14:59:44 +00:00
Claude 0606566238 feat: pin favorite web apps as embedded, swap-in-place bottom-bar tabs
Builds on the favorites system so a favorite can live in the bottom row
as a real tab instead of only launching a full-screen activity.

- FavoriteAppsRegistry gains a pinned-ids set (persisted device-locally,
  alongside the favorites list). Only WebUrl favorites are pinnable today
  — they're the ones that embed in-process — so a pinned tab always swaps
  in place and never launches an activity from the bottom row. Removing a
  favorite unpins it.
- Route.FavoriteWebApp(url) + FavoriteWebAppScreen render the embedded
  :napplet browser surface as an in-app tab: the app bottom bar stays, so
  switching to/from it is an ordinary tab swap. A pop-out action hands the
  same URL to the full-screen BrowserHostActivity for users who want it as
  its own window.
- AppBottomBar appends pinned favorites as tabs after the built-in items,
  navigating via navBottomBar (marked a tab root, so the bar stays).
- The Favorite Apps grid gains a Pin/Unpin action for WebUrl favorites.

Known follow-ups (intentionally out of this commit): NostrApp favorites
can't embed as tabs yet (they need an embedded nsite/napplet surface in
nappletHost), and embedded tabs rebuild on return rather than staying warm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 14:40:51 +00:00
Claude c21b592cb1 feat: favorite web apps — launcher, full-screen host, and bottom-bar grid
Introduces a device-local "favorite apps" system that unifies nsites,
napplets, and arbitrary web clients behind one model and three
presentation shells, while removing the editable address bar from any
running app (it now lives only in the browser launcher).

Core spine:
- FavoriteApp (commons): a sealed model with two cases — NostrApp
  (nsite/napplet, keyed by addressable coordinate so it survives code
  updates) and WebUrl. napplet-vs-nsite is recomputed from the live
  event at launch, never stored.
- FavoriteAppsRegistry (amethyst): device-local, DataStore-backed,
  StateFlow source of truth; main process only, hydrated at app start.
- FavoriteAppLauncher: dispatches a favorite to its one launch path —
  full-screen BrowserHostActivity for a URL, sandboxed NappletLauncher
  (re-resolved from LocalCache) for an nsite/napplet.

Presentation:
- BrowserHostActivity: full-screen, single-app host in the main process
  that embeds the keyless :napplet browser surface. Its own task/recents
  entry (documentLaunchMode=intoExisting), no editable URL — locked to
  the app it opened with, keeping one NIP-07 trust context per instance.
- BrowserScreen is now a launcher: an omnibox that opens each URL in its
  own host activity, plus the shared favorites grid. The address bar is
  gone from the content surface.
- FavoriteAppsScreen + FAVORITE_APPS bottom-bar item: a grid of big
  launch buttons, reused inside the browser launcher.
- StaticWebsiteCard gains a header-actions slot; a star toggle on each
  nsite/napplet card pins it (strings/store stay in the app layer).

EmbeddedBrowserSurface extracts the chrome-free surface + controller
helper so the tab, the launcher, and the host activity share one piece
of cross-process glue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 14:30:49 +00:00
Vitor PamplonaandGitHub f6ea3e1999 Merge pull request #3342 from vitorpamplona/claude/funny-cannon-txzzqf
Load author relay lists for missing addressable notes
2026-06-23 09:58:49 -04:00
Vitor PamplonaandGitHub 3b969e4b71 Merge pull request #3335 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-23 09:44:56 -04:00
Claude 128eed4a25 refactor: reuse UserFinderFilterAssembler instead of duplicating relay-list logic
Replace the self-contained AddressableAuthorRelayLoaderSubAssembler
(which duplicated UserOutboxFinderSubAssembler) with a thin bridge that
injects UserFinderQueryState entries directly into the existing
UserFinderFilterAssembler.

When EventFinderFilterAssembler detects an AddressableNote stub whose
author relay list is unknown, it subscribes that author to userFinder.
UserOutboxFinderSubAssembler already handles the kind-0/10002 fetch and
relay resolution — no logic is duplicated. Subscriptions are cleaned up
when the note loads or the EventFinder key is removed.
2026-06-23 13:34:15 +00:00
Claude 785af41c99 refactor: remove dead full-screen browser path superseded by embedded surface
The full-screen NappletHostActivity browser-mode and NappletLauncher.launchBrowser
were added alongside the embedded browser but never wired into any UI. The embedded
surface (NappletBrowserService rendered via SurfaceControlViewHost) supersedes them
and draws the address bar in the trusted main process rather than inside the sandbox,
so this removes the weaker, unused surface.

Removed:
- NappletHostActivity browser-mode (setupBrowser, address bar, per-origin bridge) —
  reverted the host activity to its pre-browser state.
- NappletLauncher.launchBrowser and the EXTRA_BROWSER_MODE/EXTRA_BROWSER_URL extras.
- The now-unused nappletHost browser strings.

Kept (used by the embedded path): the broker per-origin token mint
(MSG_MINT_BROWSER_TOKEN / MSG_BROWSER_TOKEN / KEY_BROWSER_ORIGIN) in NappletBrokerService
and NappletIpc, so NIP-07 consent stays scoped per visited origin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 13:12:00 +00:00
Claude d63bf87d2a feat: add in-app browser tab rendered from the keyless napplet sandbox
Adds a "Browser" navigation destination (drawer + pinnable bottom-nav item,
API 30+) that opens any URL. The page renders in the sandboxed, keyless
`:napplet` process and is streamed into the main activity as a cross-process
surface via androidx.privacysandbox.ui (SurfaceControlViewHost) — only pixels
and input cross the boundary, never the WebView's JS context or the NIP-07
bridge. The trusted address bar is drawn by the main process around the
embedded surface, so the sandbox can never spoof the URL.

NIP-07 `window.nostr` is injected the same way nSite website mode does it, but
scoped per visited origin: each origin gets its own broker-minted launch token
(keyed by the trusted source origin), so a grant to one site never leaks to
another.

- NappletBrowserService (`:napplet`): hosts the live-URL WebView, exposes it as
  a SandboxedUiAdapter, and relays the per-origin NIP-07 bridge to the broker.
- NappletBrowserUiAdapter: wraps the WebView session for privacysandbox.ui.
- NappletBrokerService: mints a per-origin synthetic identity so NIP-07 consent
  is scoped per host.
- EmbeddedBrowserController + BrowserScreen: bind the service, render the
  SandboxedSdkView, and drive the trusted address bar (navigate/reload/back/Tor).
- shim.js: a direct-bridge transport so the injected shim works in a top-level
  page that has no trusted shell parent.
- Browser nav item hidden below API 30 (SurfaceControlViewHost requirement).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 03:39:06 +00:00
vitorpamplonaandgithub-actions[bot] 82a20c012a chore: sync Crowdin translations and seed translator npub placeholders 2026-06-23 03:10:29 +00:00