Commit Graph
15798 Commits
Author SHA1 Message Date
Vitor PamplonaandClaude Opus 4.8 f5793937a7 Merge PR: fix(desktop): resolve Namecoin names in the home tab search bar
Merges nostr proposal ae364d99 into main: adds Namecoin (.bit) name
resolution to the desktop home-tab search bar (desktopApp FeedScreen.kt).
Network IO runs off the UI thread, stale lookups cancel via effect re-keying,
and it reuses the shared NamecoinNameResolver. (Nit deferred: extract a shared
rememberNamecoinResolution helper to de-dup with SearchScreen.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:19:23 -04:00
Vitor PamplonaandGitHub c67413544e Merge pull request #3392 from vitorpamplona/claude/ship-accrescent-6t68th
Add Accrescent APK set build & remove cleartext traffic flag
2026-06-26 18:02:53 -04:00
Vitor PamplonaandGitHub a56b5eff20 Merge pull request #3391 from vitorpamplona/docs/agent-ngit-pr-workflow
docs(claude): document the ngit + GitHub PR workflow for agents
2026-06-26 18:02:36 -04:00
Vitor PamplonaandClaude Opus 4.8 36ae65765d docs(claude): document the ngit + GitHub PR workflow for agents
Adds a `.claude/skills/ngit-pr` skill and a pointer from CLAUDE.md so
agents know how to create, review, revise, and merge PRs in this repo.

This repo can publish a PR two ways and the difference is easy to get
wrong: a GitHub remote (canonical `main`, normal `gh` flow) and a
git-over-nostr remote (`ngit`, where PRs are nostr proposals on
gitworkshop.dev and a push fans out to GitHub + the GRASP servers).

The skill identifies remotes by URL (names vary per clone; a collaborator
may have only one), explains which path to use, and documents the
three-mains alignment gate (GitHub vs the lagging nostr `main` vs local
`main`) that the nostr create/revise/merge flows all depend on — the
thing that otherwise causes rejected pushes and revisions that never
appear on gitworkshop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:55:34 -04:00
Vitor PamplonaandClaude Opus 4.8 f219153a7d Merge PR: fix(tor): wire Onion-Location interceptors into every OkHttp client
Merges nostr proposal e5865428 (v2) into main:
- fix(tor): wire Onion-Location interceptors into every OkHttp client
  (onionCache made non-nullable; OnionInterceptorWiringTest)
- refactor(napplet): route blob fetches through the shared OkHttpClientFactory
- fix(napplet): route brokered resource fetches by the applet's own Tor mode

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:41:28 -04:00
Vitor PamplonaandClaude Opus 4.8 1dbfd78b44 fix(napplet): route brokered resource fetches by the applet's own Tor mode
The consolidation passed `useProxy = true` for every brokered `resource.bytes`
fetch, forcing them through Tor whenever Tor was active — regardless of the
napplet/nSite's actual network mode. That overrides the user's explicit choice:
an nSite running in "open web" mode would still have its blob fetches tunneled,
inconsistent with how its own WebView page loads.

The authoritative per-applet preference already exists main-side in
NappletNetworkRegistry.useTor(coordinate) (locked napplets pinned to Tor;
nSites follow the persisted per-site toggle, which relaunches on change) — the
same source NappletLauncher reads to set the WebView proxy. Thread the calling
applet's coordinate through NappletResourceGateway.fetch so the broker can
resolve it, and pick the shared client with
getHttpClient(useProxy = NappletNetworkRegistry.useTor(coordinate)). This
mirrors the host's own `effectiveProxy = if (useTor) proxyPort else -1` exactly,
so a brokered fetch now routes like the applet's page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:32:57 -04:00
Vitor PamplonaandClaude Opus 4.8 c797033ba5 refactor(napplet): route blob fetches through the shared OkHttpClientFactory
The t4 onion-location proposal hand-wired OnionLocationInterceptor +
OnionUrlRewriteInterceptor into NappletResourceFetcher's private,
torPort-keyed OkHttpClient. That reached onion-routing parity but
duplicated the exact wiring OkHttpClientFactory already does, and the
private client still missed the local Blossom cache redirect, the shared
connection pool / HTTP-2 keepalive, and SurgeDns.

Inject the app-wide client instead: NappletResourceFetcher now takes a
() -> OkHttpClient and the broker supplies
`okHttpClients.getHttpClient(useProxy = true)` — the same DualHttpClientManager
path the image pipeline uses. Behavior-preserving for Tor (proxied when
Tor is active, clearnet when not) and, since these are sha256 blobs, the
shared Blossom-cache redirect is now a feature, not a loss. Drops the
private client + its cache and the hand-wired interceptors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:32:57 -04:00
mstrofnoneandVitor Pamplona 6209378b83 fix(tor): wire Onion-Location interceptors into every OkHttp client
Pins the Onion-Location interceptor wiring introduced in #3368 across
every OkHttp client the app builds and closes two gaps the audit
surfaced:

* OkHttpClientFactory.onionCache was nullable with a null default.
  A future call site constructing the factory without explicitly
  passing the cache would silently disable onion-routing for the
  entire HTTP role (image, upload, money, NIP-05, preview, push).
  Tightened to required (matches DualHttpClientManagerForRelays).

* NappletResourceFetcher built a raw OkHttpClient with no interceptors.
  Napplet HTTPS / blossom fetches over Tor would hit clearnet exit
  nodes even when the destination advertised an onion. Wired through
  the app-wide OnionLocationCache so a hint learned anywhere in the
  app applies, and vice versa.

ElectrumX is intentionally excluded: it uses raw Socket/SSLSocket,
not OkHttp, so the Onion-Location HTTP header does not apply. Tor
routing for ElectrumX continues to go through the Tor-aware
SocketFactory plus the Namecoin _tor field on the record (a
stronger, blockchain-anchored trust path than a passive HTTP hint).

IsEmulator is made null-safe (each Build.* field coalesced to "") so
unit tests can stand up the affected classes without NPEing on the
JVM default-values stub of android.os.Build.

New OnionInterceptorWiringTest (11 cases) covers:
  * locationInterceptor records the header under the clearnet host
    (HTTPS path and WebSocket 101 upgrade)
  * locationInterceptor with no header writes nothing
  * rewriteInterceptor passes through unknown hosts
  * rewriteInterceptor https -> https.onion preserves scheme
  * rewriteInterceptor https -> http.onion downgrades safely
  * rewriteInterceptor passes through unparseable cache values
  * cache round-trip and shared-instance invariant
  * compile-time pin that both classes remain okhttp3.Interceptor

Build:
  ./gradlew --no-daemon spotlessCheck                                       OK
  TZ=UTC ./gradlew --no-daemon :amethyst:testPlayDebugUnitTest              763 tests, 0 failures
  ./gradlew --no-daemon :commons:verifyKmpPurity :quartz:verifyKmpPurity    OK
2026-06-26 17:32:57 -04:00
Claude 284b7b143e ci: build signed Accrescent APK set (.apks) on release
Accrescent only accepts a signed APK set of split APKs generated by
bundletool from an AAB — not the AAB itself and not a monolithic APK.
After signing the F-Droid AAB, run bundletool build-apks (--mode=default)
to emit dist/amethyst-fdroid-<tag>.apks, signed with the same release
keystore secret the other signing steps use. It is attached to the GitHub
Release via the existing dist/* glob.

Upload to Accrescent stays manual (drag the .apks into the developer
console): Accrescent has no publish API or CI CLI yet — both are on their
roadmap but unreleased. A build-time guard warns if the APK set exceeds
Accrescent's 128 MiB automated-check limit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rE4k6sUN39emsofSv1Y1M
2026-06-26 21:27:16 +00:00
Vitor PamplonaandGitHub 2e69f85255 Merge pull request #3389 from vitorpamplona/claude/pr-rendering-layout-uafuio
Style Git pull request clone URLs with labelMedium typography
2026-06-26 17:19:45 -04:00
Vitor PamplonaandGitHub 1788c93358 Merge pull request #3388 from vitorpamplona/claude/browser-nsite-preload-ekriqv
Preload favorited nSites/nApplets to avoid "isn't loaded yet" toast
2026-06-26 17:05:21 -04:00
Claude e70688841c fix: preload favorited nsite/napplet manifests in the browser
Favorites store only the addressable coordinate (kind:pubkey:dtag); the
launch path re-resolves the live event from LocalCache at tap time. When
that event hadn't streamed in yet, tapping a favorited nsite/napplet showed
"isn't loaded yet" and never recovered on its own — the only thing that
pulled the event into the cache was visiting the nsite/napplet feed (it
subscribes by author), which is why opening that feed and coming back made
the favorite suddenly launchable.

Add PreloadFavoriteNostrApps, which subscribes each favorited coordinate to
the shared EventFinder (the same lifecycle-aware loader observeNote uses) so
the manifests fetch via the author's outbox relays as soon as the launcher
opens. Wire it into the Browser tab and the Favorite Apps tab. The loader
drops each coordinate once its event arrives, so this is a one-shot fetch,
not a standing feed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH7D6xUhHDKUbrYHoBnZyL
2026-06-26 20:53:46 +00:00
Claude 9002c63916 feat: reorder PR card with repo header on top and size clone links
Move the project's name (repository) row to the top of the pull-request
card, above the type/status row. Render the clickable clone download
links at the same font size as the branch/commit/merge-base meta rows so
the metadata block reads as one consistent sequence. Apply the same link
sizing to the PR update card for consistency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FjxBXAx2NQE6xi3TLBNJ23
2026-06-26 20:44:12 +00:00
Claude 11a2474338 fix: drop redundant usesCleartextTraffic flag for Accrescent compliance
Accrescent's automated checks reject any app whose manifest sets
android:usesCleartextTraffic="true". The attribute is already inert on
this app: it is ignored on API 24+ whenever a networkSecurityConfig is
present, and minSdk is 26, so cleartext is governed entirely by
network_security_config.xml (base-config cleartextTrafficPermitted=true).

Removing the attribute is behavior-preserving — user-configured ws://
relays on local IPs and the 127.0.0.1 Tor SOCKS proxy keep working via
the network security config — while satisfying Accrescent's check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rE4k6sUN39emsofSv1Y1M
2026-06-26 20:33:52 +00:00
Vitor PamplonaandGitHub 52a467db5f Merge pull request #3387 from vitorpamplona/claude/zap-card-bg-color-kz8szr
Fix activity card background propagation to embedded notes
2026-06-26 16:26:02 -04:00
Claude d6fc3232b2 fix: keep zap/reaction card inner content on the card's tint
The embedded note and comment inside the lightning / nutzap / onchain-zap
and reaction activity cards were handed the parent feed / MultiSetCard
background state, so they drew black (the app background) or flashed the
new-note highlight instead of letting the card's orange (or like-tinted)
wash show through.

ActivityCardFrame now exposes a stable transparent background state to its
content for the inner note and comment to draw on; the hand-built onchain
card uses a matching local state. Nothing inside the card's layout changes
background from the feed anymore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GEUsgVr5e31qYeqNSjgHif
2026-06-26 19:37:43 +00:00
Vitor PamplonaandGitHub 19debffe15 Merge pull request #3384 from vitorpamplona/claude/nsite-napplet-icons-okz5y6
Show nSite/nApplet icons from bundled blobs in favorites
2026-06-26 15:16:43 -04:00
Claude c7e14676f2 Merge remote-tracking branch 'origin/main' into claude/nsite-napplet-icons-okz5y6 2026-06-26 19:11:40 +00:00
Claude e8539df81e Merge remote-tracking branch 'origin/main' into claude/nsite-napplet-icons-okz5y6
# Conflicts:
#	amethyst/src/main/res/values-fr-rCA/strings.xml
2026-06-26 19:07:03 +00:00
Vitor PamplonaandGitHub b703330c6a Merge pull request #3386 from vitorpamplona/claude/amethyst-lint-translations-dend0l
Move browser strings to commons for shared localization
2026-06-26 15:06:54 -04:00
Vitor PamplonaandGitHub 7fc5c92760 Merge pull request #3385 from vitorpamplona/fix/account-dir-cleanup-on-delete
fix: delete per-account dir on account removal + sweep orphans at startup
2026-06-26 15:05:42 -04:00
Claude 0092b4f726 perf: single-pass icon-path selection; resolve on event not NoteState
NappletIconPath.choose scanned the whole path list once per conventional name
(O(PRIORITY x N)), allocating a filter list and recomputing each basename up to
20 times per path. A manifest is a whole static site, so N can be large. Collapse
to one O(N) pass with a name->rank map: basename computed once per path, no
intermediate lists, and the loose-raster fallback is skipped entirely once an
exact match is in hand. Behavior is unchanged (the 11 selection tests still pass).

Also key the favorite-icon resolution on the manifest event rather than the whole
NoteState, so paths()/choose() don't re-run on unrelated metadata bumps (a
reaction or zap tracked on the note).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaPdt8EnSrSBuzdudrxzTH
2026-06-26 19:00:53 +00:00
Claude 5a297f17a1 fix: move orphaned browser/napplet translations to :commons
The browser/napplet strings (browser_address_hint, browser_console_title,
browser_console_title_short, browser_console_clear, napplet_untitled) were
moved to :commons, but their per-locale translations were left behind in
amethyst's values-*/strings.xml. With the default keys gone from amethyst,
lint flagged them as ExtraTranslation (80 errors across 16 locales).

Move the translations into commons/src/androidMain/res/values-*/strings.xml
so the default key and its translations live in the same module, preserving
the existing translation work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uza7sGxYPZtY43Ln2yH8FQ
2026-06-26 18:58:08 +00:00
Vitor PamplonaandGitHub 0afc83dcfb Merge pull request #3383 from vitorpamplona/claude/amethyst-draft-signing-bug-9cx0zx
Fix draft deletion by holding strong reference to draft notes
2026-06-26 14:52:24 -04:00
Vitor PamplonaandClaude Opus 4.8 58b551265c fix: delete per-account dir on account removal + sweep orphans at startup
Account deletion cleaned up the saved-accounts list and encrypted prefs but
never removed the on-disk files/accounts/<pubkey>/ directory (the MLS/Marmot
stores created in AccountCacheState.loadAccount). Every deleted or logged-out
account leaked its folder, so the on-disk account count drifted far above the
number of accounts shown in the switcher (16 dirs vs 6 saved on a test device).

- AccountCacheState: add deleteAccountFiles(pubkey) to remove the directory and
  pruneOrphanAccountDirs(keepPubkeys) to clear dirs no longer backed by a saved
  account.
- AccountSessionManager.logOff: delete the files in both delete branches.
- AppModules: one-time startup sweep, keyed by the hex of every saved account,
  to clean up folders leaked before this fix.
- LocalPreferences.savedAccounts(): make the lazy init race-safe with a
  dedicated mutex + double-checked locking. Multiple startup coroutines
  (account load, always-on notification service, the new orphan sweep) call it
  concurrently; the old check-then-act could run the IO read in parallel and
  double-write ALL_ACCOUNT_INFO during the legacy migration.

Verified on-device: 16 -> 6 account dirs after one launch, stable across
restarts, no startup regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:47:23 -04:00
Claude 119517bd3a fix: only resolve the draft note when there is a version to save
The versions collector resolved draftNote = account.getOrCreateDraftNote(...)
on every emission, including the content-less initial tick (~1s after the
composer opens). That touched the lateinit account before any user input; if
it were ever unset at that moment the throw would kill the collectLatest
coroutine and silently stop all draft saves for that composer.

Move the resolve inside the `if (it > 0)` guard, co-located with the save, so
account is only read once a real edit exists. The open-a-draft-then-send-
without-editing case is still covered by the explicit refresh in load().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
2026-06-26 18:41:23 +00:00
Claude bb968f74f2 fix: make nsite/napplet favorite icon reactive to manifest arrival
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaPdt8EnSrSBuzdudrxzTH
2026-06-26 18:23:52 +00:00
Claude 94b99e0973 refactor: derive the draft note from the versions flow, decouple DraftTagState
DraftTagState goes back to pure tag/version state — it no longer knows about
AddressableNote or needs an account-aware builder. Instead each composer
derives draftNote from the debounced versions collector it already runs:
on each emission it maps the current tag to its live cache note via
account.getOrCreateDraftNote(current). The ViewModel field holds the strong
reference that keeps LocalCache's weak entry alive until a deletion needs it.

load() refreshes draftNote after set(oldTag) because set() doesn't bump
versions, covering the open-a-draft-then-send-without-editing case.

deleteDraftInner takes the nullable note again and still only signs when the
note holds a real, non-deleted DraftWrapEvent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
2026-06-26 18:16:42 +00:00
Vitor PamplonaandGitHub 799bbe68ef Merge pull request #3382 from vitorpamplona/claude/amethyst-logo-redesign-nlefdg
Redesign service notification icon with network topology
2026-06-26 14:11:55 -04:00
Claude 5b8a42020c feat: adopt orbit-ring service notification icon; drop preview scaffolding
Make the orbit design (centred gem ringed by three "server" nodes) the
always-on relay-service notification icon by writing it into the existing
amethyst_service drawable, so NotificationRelayService picks it up unchanged.

Remove the icon bake-off scaffolding now that a design is chosen:
- delete the alternative drawables (amethyst_service2..6)
- delete the DEBUG-only ServiceIconPreviewNotifications helper
- drop its trigger and now-unused imports from MainActivity

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
2026-06-26 18:04:42 +00:00
Claude 6b6ee26a88 style: maximize centred gem in the service-icon options
Grow the central Amethyst gem in each motif (orbit/sync/hub/waves) to the
largest size that still clears the surrounding elements. For the hub icon the
spokes now start farther from the centre to give the gem room.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
2026-06-26 17:57:31 +00:00
Claude 111686884a fix(i18n): drop CDATA from French "Block & Hide" strings
block_hide_user and report_dialog_block_hide_user_btn wrap their value in
<![CDATA[...]]> in the English source only because the text contains a literal
'&' (Block & Hide User). The French translations kept the CDATA wrapper but the
text has an apostrophe (l'utilisateur) instead — and AAPT2 fails to flatten a
CDATA-wrapped apostrophe ("Can not extract resource from ParsedResource" /
"Invalid unicode escape sequence"), breaking mergePlayDebugResources.

Drop the now-pointless CDATA and use a normally-escaped apostrophe (l\'utilisateur)
— identical runtime string, valid under every AAPT2 version, and consistent with
the escaped-apostrophe style the rest of these files already use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaPdt8EnSrSBuzdudrxzTH
2026-06-26 17:45:21 +00:00
Claude eb6dfc7a4a feat: show nsite/napplet bundled icon on favorite tabs
Favorited web apps replace the bottom-nav glyph with the site's favicon, but
nsites (NIP-5A) and napplets (NIP-5D) fell back to the generic grid glyph.

The webapp trick (capturing onReceivedIcon from the WebView) can't work here:
nsites/napplets render inside a cross-origin sandboxed iframe under a trusted
shell, so onReceivedIcon only ever reports the shell's main-frame icon, never
the applet's. Instead, derive the icon from the manifest's own bundled blobs —
content-addressed, sha256-verified, and Tor-routed like the rest of the site.

- quartz: NappletIconPath picks the best conventional icon path (favicon/icon/
  apple-touch-icon, raster over ico/svg, shallower path wins) from a manifest's
  path tags; exposed as iconBlob() on the four nsite/napplet event kinds. Unit
  tested.
- amethyst: rememberNappletIconModel resolves the live manifest from LocalCache,
  prefetches the icon blob into the shared verified cache off the composition
  thread, and returns a file:// model. AppBottomBar and FavoriteAppsScreen feed
  it into FavoriteAppIcon for NostrApp favorites, mirroring the webapp path.

Priority: bundled blob > manifest icon URL tag > type glyph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaPdt8EnSrSBuzdudrxzTH
2026-06-26 15:55:12 +00:00
Claude 002c8b6d30 style: enlarge service-icon options to fill the icon frame
Scale up the orbit/sync/hub/waves motifs and their centred gems so each
drawing occupies as much of the 512 viewport as possible (rings and nodes
pushed near the edge, larger central gem), keeping a small margin so strokes
don't clip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
2026-06-26 15:53:11 +00:00
Claude b5706564d9 refactor: build the draft note from the tag inside DraftTagState
Instead of capturing the AddressableNote returned by each save (and on
load) and re-assigning it, DraftTagState now builds the note from the
current tag via an injected (tag -> AddressableNote) builder and rebuilds
it whenever the tag changes. Because that note is the live cached object
for the address, its event tracks the draft automatically as it is saved
or removed, so:

- the note is never null inside the state (lateinit, wired by start());
- createAndSendDraftIgnoreErrors no longer needs to return the note, and
  load no longer needs to capture it — set(oldTag) rebuilds it;
- the writer's existence check becomes "is there a real, non-deleted
  draft event in the note" (DraftWrapEvent.isDeleted()), which also stops
  a second blank-delete from re-signing an already-emptied draft.

ViewModels just wire draftTag.start(account::getOrCreateDraftNote) in
init() and reference draftTag.note; the per-VM field and held() are gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
2026-06-26 15:38:55 +00:00
Claude e1c152ada8 feat: add four more service-icon options + on-device preview harness
Add four additional always-on service notification icon candidates, each
keeping the centred brand gem with a "background service / connected to
servers" motif around it:

- amethyst_service3: orbit ring with three server nodes
- amethyst_service4: two looping sync arrows (running)
- amethyst_service5: hub & spoke to five nodes (connected to relays)
- amethyst_service6: concentric broadcast waves

To compare all candidates on a real device, add a DEBUG-only helper
(ServiceIconPreviewNotifications) that posts one always-on-style ongoing
notification per icon (same channel style, ongoing/silent/low priority),
triggered from MainActivity.onCreate under BuildConfig.DEBUG.

NOTE: the preview harness (ServiceIconPreviewNotifications + the
MainActivity hook) is temporary scaffolding for the icon bake-off and is
meant to be removed once a design is chosen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
2026-06-26 15:33:15 +00:00
Vitor PamplonaandGitHub 6b7e541f55 Merge pull request #3380 from vitorpamplona/claude/adoring-babbage-stzzck
Auto-save Cashu wallet on mint add/remove, remove manual key picker
2026-06-26 11:16:48 -04:00
Claude a242411df6 refactor: own the draft note reference inside DraftTagState
The strong reference that keeps a saved draft alive (so LocalCache's weak
reference can't collect it before deletion) was duplicated as a draftNote
field across all eight composer ViewModels, each re-clearing it in
cancel(). The note's lifecycle is 1:1 with the draft tag, so DraftTagState
is its natural owner: it now holds the AddressableNote, exposes held() to
set it, and drops it in rotate() — which every cancel() already calls.

ViewModels now reference draftTag.note / draftTag.held(...) and no longer
carry their own field or reset logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
2026-06-26 15:15:42 +00:00
Claude b4d39a7e52 feat: auto-publish Cashu wallet on mint add/remove, drop Save button
The Add/Edit Cashu wallet screen no longer has a Save button. The NIP-60
wallet (kind:17375) and nutzap info (kind:10019) are now published the
instant the first mint is added, and re-published on every later add or
remove — so the list on screen always matches what's on relays.

This removes the confusing two-step flow where a typed/selected mint plus a
chosen key still left Save disabled until the user discovered the "+" button.

The explicit P2PK key picker (auto-generate / paste) is gone from this
screen: a nutzap key is generated automatically on first creation. Advanced
key import/rotation still lives in the settings Danger Zone.

Key safety: publishMints reuses the wallet's existing P2PK key on every
re-publish. The first publish's key is cached in the ViewModel and guarded
by a mutex, so rapid successive adds — firing before the new kind:17375
round-trips back through LocalCache — can't generate a second key and
rotate it, which would orphan inbound nutzaps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX5wkwXbvoQpadALx1ZVjk
2026-06-26 15:09:20 +00:00
Vitor PamplonaandGitHub 356e1431d8 Merge pull request #3378 from vitorpamplona/fix/embed-ime-selection
Embedded text selection: native-parity IME + selection UI + magnifier, and platform-bug fixes
2026-06-26 11:08:42 -04:00
Vitor PamplonaandGitHub 65f5292f6b Merge pull request #3379 from vitorpamplona/claude/notecompose-zap-split-preview-dgn0js
Show boosted notes as compact preview in activity cards
2026-06-26 11:08:17 -04:00
Claude 50dbbeeecc fix: hold a strong reference to the draft note for reliable deletion
LocalCache.addressables keeps AddressableNotes via WeakReference, so a
saved draft could be garbage-collected between creation and deletion.
The previous existence check (look the draft up by tag before signing)
would then find nothing locally and skip the deletion, leaving an orphan
draft on the relays.

Each composer ViewModel now holds a strong reference to its draft note:
createAndSendDraftIgnoreErrors returns the consumed AddressableNote, and
load()/editFromDraft captures the note when editing an existing draft.
deleteDraftInner takes that held note directly (sourcing the dTag and
relays from it) instead of a tag lookup, so it can always reach the draft
it needs to delete and still signs nothing when there is no draft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
2026-06-26 14:59:44 +00:00
Vitor PamplonaandClaude Opus 4.8 11e1e17655 docs(embed): record session fixes in the native-parity plan
Mark the full-screen round-trip corruption bug FIXED (process-global pauseTimers
+ attached WebView.destroy), and add the no-blink word-select, page-selection-
clears-on-focus, caret-handle-drag (positionChangeIgnoreConsumed), hybrid
word+char extend, and embed app-theme (nightThemedContext) fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:57:36 -04:00
Claude bacb8a57ad feat: add circular-badge service icon as amethyst_service2 option
Keep the original hollow-outline amethyst_service icon and add the new
circular badge (solid disc with the gem punched out of its centre) as a
separate amethyst_service2 drawable, so the always-on notification icon
can be switched between the two without losing either design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi
2026-06-26 14:57:22 +00:00
Claude bf2dc03a50 fix: escape apostrophe inside CDATA in fr-rCA strings
The Crowdin-synced values-fr-rCA/strings.xml had unescaped apostrophes inside the
CDATA sections of block_hide_user and report_dialog_block_hide_user_btn, which
aapt2 rejects ("Invalid unicode escape sequence"), breaking resource compilation.
Escape them as \' to match the known-good values-fr translations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4rrFiWApq8EFpk4fSuTFH
2026-06-26 14:54:09 +00:00
Vitor PamplonaandGitHub f458fdaa8a Merge pull request #3377 from vitorpamplona/claude/amethyst-git-notifications-rqfkux
Add Git pull request event rendering support (NIP-34)
2026-06-26 10:53:30 -04:00
Claude 120c31ac95 feat: always show short preview for posts inside zap activity cards
The lightning, nutzap, and onchain activity cards embed the zapped post via
RenderZappedPost with makeItShort = true. The 2-line compact preview, however,
only triggered when the logged-in user authored the post
(makeItShort && isLoggedUser(author)). When the user is merely a zap-split
beneficiary of someone else's post, that check failed and the post rendered in
full instead of the intended compact preview.

Gate the short preview on the boosted-note flag as well, so any post embedded in
one of these activity cards (the only makeItShort callers that pass
isBoostedNote = true) is always shown as a 2-line preview, regardless of author.
Other makeItShort callers (Report, community post approval, attestation, reply
composition, compose screens) all use isQuotedNote instead, so their behavior is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4rrFiWApq8EFpk4fSuTFH
2026-06-26 14:49:47 +00:00
Vitor PamplonaandClaude Opus 4.8 43aebcca63 fix(embed): make embedded + full-screen WebViews follow the app theme
The embedded in-app browser and napplet/nSite surfaces rendered web content in
the device theme, ignoring the app's DARK/LIGHT preference — a site with dark
support stayed light when the app was dark (and vice versa). The full-screen
activities had the same latent gap (they followed the device, not the app).

Root cause: WebView's dark decision (prefers-color-scheme via algorithmic
darkening) reads the context's THEME (?android:attr/isLightTheme), not just the
Configuration uiMode. The off-window SurfaceControlViewHost surface context
carries neither the host window's theme nor its night mode, so the renderer came
up light. The old applyNightMode() used UiModeManager.setNightMode — a
permission-gated no-op — so the theme never reached the WebView at all.

Fix: build every embed/host WebView from nightThemedContext() — a
ContextThemeWrapper over a forced-night/day Configuration with a DayNight theme,
so the theme's isLightTheme resolves from the app's resolved theme. Shared in
EmbedWebViewTheme.kt; used by NappletBrowserService, NappletHostService,
NappletBrowserActivity, and NappletHostActivity. Removed the dead applyNightMode
no-op from all four. (Verified on device with a throwaway SurfaceControlViewHost
repro: config-only context does NOT work; setForceDark is a no-op at targetSdk
37; setApplicationNightMode does nothing; the DayNight ContextThemeWrapper is
what flips the renderer, even across the cross-process embedded surface.)

Device-verified: all four surfaces follow the app theme even when it differs
from the device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:49:44 -04:00
Claude 3c041d2915 feat: modern git cards for pull requests, plus issue/patch polish
NIP-34 pull requests (kind 1618) and pull-request updates (kind 1619)
previously had no renderer and fell through to the plain text-note
branch, so a PR notification opened onto an unstyled markdown blob with
none of its structured data shown. Add dedicated cards that reuse the
existing patch/issue card vocabulary (bordered container, type chip,
status pill, embedded repository header) and surface the PR-specific
metadata the event carries:

- Pull Request card: "Pull Request" chip with a merge glyph, status
  pill, subject title, branch name, current commit, merge base, and
  clone-URL download rows.
- PR Update card: "PR Update" chip, repository header, new commit /
  merge base, clone URLs, and an explanatory line (updates carry no
  body content).

While here, modernize the existing cards consistently:

- Factor the shared markdown body, metadata row, and subject title into
  reusable composables (GitMarkdownBody / GitMetaRow / GitSubjectTitle).
- Show the issue subject as a proper title. The old code cast the event
  to TextNoteEvent to read the subject, which always returned null
  (GitIssueEvent is not a TextNoteEvent), so issue subjects were never
  displayed; read it from GitIssueEvent.subject() instead.
- Render the patch commit as an iconed metadata row.

Wire the two new kinds into NoteCompose and the thread detail view, add
the CallMerge / Commit / AltRoute Material symbols (font subset
regenerated), and add the new string resources.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
2026-06-26 13:39:05 +00:00
Claude df5361d220 fix: don't sign a draft deletion when no draft exists
deleteDraftInner() unconditionally signed a deleted-draft wrap event plus
a deletion event, even when no draft for that tag existed. With an
external NIP-55/NIP-46 signer, clearing a composer to blank could then
prompt for a signature even when "Automatically create drafts" is off,
since the blank-text branch of sendDraftSync() always calls delete.

Skip both signatures when the draft does not exist in the cache. The
existence lookup reuses the addressable note already fetched for relay
hints, so it adds no extra work, and real drafts are still deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
2026-06-26 13:38:20 +00:00