Commit Graph
998 Commits
Author SHA1 Message Date
nrobi144 33bb81dddb Merge remote-tracking branch 'upstream/main' into feat/desktop-privacy-lock
# Conflicts:
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt
2026-07-02 07:20:44 +03:00
Claude b1fda59cd6 fix: drop relay.damus.io from default relay lists ahead of shutdown
relay.damus.io is being decommissioned, so remove it from every runtime
default/fallback relay set to stop the app and amy from wasting connection
slots on a dead host:

- commons Constants: remove `damus`; it dropped out of `bootstrapInbox`
  (default NIP-65 inbox) and `eventFinderRelays` (default outbox/fallback),
  both still carrying 6 healthy relays.
- ChessConfig: remove damus from CHESS_RELAYS / CHESS_RELAY_NAMES, leaving
  the 3 relays the FETCH_TIMEOUT comment already assumes.
- desktop DefaultRelays: remove damus and the also-dead relay.snort.social.
- desktop FollowPacks DISCOVERY_RELAYS: remove damus.
- amy NipCommand SEARCH_RELAYS: swap damus for the NIP-50-capable nostr.wine.

Comments, @Preview sample data, and test fixtures that mention damus.io are
left untouched — they have no runtime effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Ttcqa3V78bugGraGhtehj
2026-07-01 23:35:24 +00:00
Claude dc9579f994 Merge remote-tracking branch 'origin/main' into claude/podcast-event-kinds-merge-vv24gd 2026-07-01 18:39:59 +00:00
nrobi144 72c3dff870 feat(privacylock): P0 security hardening — 600k iterations + backoff
Addresses the P0 items in the security review at
docs/plans/2026-07-01-privacy-lock-security-review.md.

## PBKDF2 iterations 100k → 600k (M1) via versioned hash format (M2)

- New PasswordHasher storage format: `v1$saltB64$hashB64` (600k
  iterations, matches OWASP 2023 Password Storage Cheat Sheet for
  PBKDF2-HMAC-SHA256).
- Legacy `saltB64$hashB64` (100k iterations) format still verifies
  correctly — no user gets locked out by the bump.
- `hash()` always produces `v1$…`; users migrate to v1 opportunistically
  when they Change or Set a new password.
- New `PasswordHasher.isLegacyFormat()` helper for callers that want
  to force-migrate on next successful unlock.
- Verify cost goes from ~50ms → ~250ms on a modern laptop — well
  within tolerable UX for a lock users open a handful of times per
  session.

## Exponential backoff on failed unlock (M3)

- `PrivacyLockSettings` gains `failedUnlockAttempts: StateFlow<Int>`
  and `lockedUntilEpochMs: StateFlow<Long?>`, both persisted via
  java.util.prefs so a reboot cannot reset the backoff.
- `MessagesLockState.onFailedUnlockAttempt(nowMs)` implements the
  schedule: no lockout for first 4 fails, then 30s / 60s / 120s /
  300s (capped at 5 min).
- `MessagesLockState.onUnlockSuccess()` transparently clears the
  attempt counter and any active lockout (also called from the
  banner-enable path).
- DesktopLockScreen shows a countdown ("Try again in 27s") in the
  supportingText, disables the password field and Unlock button
  during lockout, ticks every 500ms via a LaunchedEffect.
- RemovePasswordDialog inherits the same protection — Settings can't
  bypass the throttle by disabling the lock.
- 4 new unit tests cover threshold behavior, base trip, doubling +
  cap, reset on success. All 13 tests green.

## Not in this commit

- L1/L2 (String/CharArray memory retention) — out-of-tree fix in
  Compose; accepted per threat model.
- L3 (post-uninstall prefs) — release-notes item.
- M4 (Limitations copy update) — deferred; existing "does not
  protect against filesystem access" line already covers.
2026-07-01 12:06:45 +03:00
nrobi144 d216d22c3e feat(privacylock): Messages first-run discovery banner (Desktop)
Adds an inline banner at the top of the Desktop Messages deck column
that nudges users to enable the privacy lock. Fires only when
!lockEnabled && !firstRunCardSeen; dismissal is sticky across
restarts + lock enable/disable cycles.

- MessagesFirstRunBanner: AnimatedVisibility(expandVertically + fadeIn)
  wrapper around a Surface + Row with a padlock icon, title, body,
  and Enable / Not now buttons. Modeled on OfflineBanner.kt.
- SetPasswordDialog extracted from PrivacyLockSettingsScreen.kt into
  a shared desktop/security/ file so the banner and the settings pane
  both point at the same composable.
- MessagesLockState.onUnlockSuccess() relaxed to accept Disabled as a
  valid previous state, so enabling from the banner keeps the user
  Unlocked and doesn't flash the lock screen. New unit test covers
  this path; all 9 tests green.
- DesktopMessagesScreen wraps its two-pane / compact layout in a
  Column with the banner on top and a Box(weight(1f)) around the
  panes so fillMaxSize propagates correctly.
2026-07-01 11:35:52 +03:00
nrobi144 1c0141aba1 feat(privacylock): Desktop wiring — gate, password unlock, settings
Phase 5 (Desktop-only). Wraps the Messages deck column behind a
PBKDF2-hashed password gate; drops the Android-app slice.

- PrivacyLockSettings gains passwordHashed field + setter (salt$hash,
  base64). Backed by java.util.prefs on desktop.
- PasswordHasher: PBKDF2-HmacSHA256, 100k iterations, 16-byte salt,
  256-bit key, constant-time compare. Same primitive family as
  SecureKeyStorage.
- DesktopMessagesLockGate: synchronous branch select in composition
  (no LaunchedEffect guard) — closes the deep-link race per plan
  §Security Hardening H1. Renders content when Disabled/Unlocked;
  renders inline password TextField when Locked. Fires
  MessagesLockState.onLeaveRoute() in DisposableEffect onDispose so
  navigating away from the Messages column re-locks immediately.
- DesktopMessagesLockGate handles the "no password set" edge case
  with a Disable-lock affordance.
- LocalPrivacyLockSettings CompositionLocal + LocalMessagesLockState
  (from commons) both provided once at the App composition root in
  Main.kt. Constructed with the existing windowScope so the state
  holder's idle timer coroutines are lifecycle-scoped to the Window.
- DeckColumnContainer: DesktopMessagesScreen wrapped in
  DesktopMessagesLockGate for the Messages column.
- Desktop PrivacyLockSettingsScreen: Column + Card layout matching
  LocalRelaySettingsScreen (no Scaffold). Toggle, "Change password"
  affordance with a full set/change dialog (old + new + confirm),
  inactivity timer dropdown (1m / 5m / 15m / 1h / Never), redaction
  level dropdown (Hidden / Full), honest limitations copy. Auto-opens
  the set-password dialog if user toggles ON with no password set.
- Slotted into the existing Settings pane in Main.kt right after
  LocalRelaySettings.
2026-07-01 11:35:52 +03:00
nrobi144 c4f647d01a feat(privacylock): foundation — state holder, settings, gate composable
Phase 1 of the messaging privacy lock. Headless cross-platform spine in
commons; no UI wiring yet.

- LockState sealed interface (Disabled / Locked / Unlocked)
- InactivityTimer enum (1m / 5m / 15m / 1h / Never; default 5m)
- DmRedactionLevel enum (Generic / Full)
- PrivacyLockSettings interface (StateFlows + mutators)
- PreferencesPrivacyLockSettings backed by java.util.prefs in jvmAndroid
  (shared by Desktop + Android; node com/vitorpamplona/amethyst/privacylock)
- MessagesLockState — app-global state holder; initial value seeded
  synchronously from prefs to close the deep-link race; LocalMessagesLockState
  CompositionLocal provided at App root
- CredentialPrompter interface + PromptResult enum +
  LocalCredentialPrompter CompositionLocal
- MessagesLockGate composable — synchronous branch select (no
  LaunchedEffect guard); LockScreen with biometric button
- IdleTimerModifier — pointerInput Initial-pass, non-consuming
- 8 unit tests covering cold-start seed, idle expiry, leave-route,
  Never timer, user-interaction reset, settings cascade,
  credential-unavailable disable. All green.
2026-07-01 11:35:51 +03:00
nrobi144 d0646acf3c Merge upstream/main into feat/desktop-hashtag-spam-filter 2026-07-01 09:53:09 +03:00
nrobi144 7a18e30fc4 feat(desktop): hashtag-spam filter with collapse-with-reveal
Damus-inspired content filter that collapses notes abusing `t` hashtag
tags into a compact reveal-on-click placeholder. Ships default ON with a
threshold of 5 (adjustable 1–20 in Settings → Content Filters, or off).

Scope
- Pure check (`HashtagSpamCheck`) + settings interface
  (`HashtagSpamSettings`) live in `commons/moderation/`, callable by
  Desktop, `amy` CLI, and (future) Android.
- JVM-backed `PreferencesHashtagSpamSettings` writes to the shared
  `java.util.prefs` node `com/vitorpamplona/amethyst/filters`, so `amy`
  and Desktop observe the same value automatically.
- `CollapsedSpamNote` placeholder in `commons/ui/note/` takes only
  primitive scalars so Android can adopt it without touching commons.
- Desktop wraps every `NoteCard` call site (FeedNoteCard, QuotedNoteEmbed,
  BookmarksScreen, 5 SearchResultsList sites) with a shared
  `SpamCheckedNoteRender` helper. Thread root notes auto-expand via
  `forceReveal=true`; replies still respect the filter.

Exemptions
- Long-form articles (kind 30023)
- Authors in the follow list plus self
- Repost wrappers check the inner event's tags via precomputed
  `note.replyTo`, falling back to `containedPost()`

Search UX fixes bundled in
- Removed the `#hashtag` → "Direct lookup" card. `QueryParser` already
  extracts `#xxx` into the query's hashtag filter, so typing `#bitcoin`
  now goes straight to filtered results.
- Search-result rows now trigger metadata loading via
  `subscriptionsCoordinator.loadMetadataBatched(authors)` and observe
  each user's metadata flow via a new `rememberDisplayData` helper, so
  display names + avatars refresh when kind-0 arrives from index
  relays. Same helper reused in Bookmarks.

Tests + docs
- 19 unit tests (check × 10, displayed-event unwrap × 4, prefs × 5),
  all green.
- Manual testing sheet with 16 scenarios at
  `desktopApp/plans/2026-06-29-hashtag-spam-filter-manual-testing-sheet.md`.
- Plan at `docs/plans/2026-06-29-feat-desktop-hashtag-spam-filter-plan.md`.
- Cross-client desktop feature backlog reference at
  `desktopApp/plans/_desktop-feature-backlog.md`.
2026-07-01 09:46:43 +03:00
Claude 13d654f6be Merge remote-tracking branch 'origin/main' into claude/podcast-event-kinds-merge-vv24gd 2026-06-30 19:23:05 +00:00
Claude 6579b5f656 docs: audit, status-stamp, and index all module plans
Audited all 143 plan files across the 10 plans/ folders. Each plan now
carries a Status header (shipped | in-progress | queued | abandoned)
backed by codebase evidence, and every folder has a README.md index
grouping plans by status.

Shipped plans were moved into a per-folder plans/archive/ (via git mv,
history preserved) so each plans/ folder surfaces only live work:

  shipped (archived): 122   in-progress: 8   queued: 7   abandoned: 4

docs/plans/ is the frozen legacy folder; its plans were stamped and
indexed in place (48 of 52 archived) but it remains closed to new plans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
2026-06-30 15:35:38 +00:00
nrobi144 de2c970e3e feat(desktop): Follow Packs (NIP-51 kind 39089) discovery and follow flow
Adds a Follow Packs experience to Amethyst Desktop:

- New "Discover" sidebar destination with featured-pack hero, hashtag chips
  driven by NIP-12 `t` tags, a 3-up "From the pack" notes feed, and a
  right-rail of mini pack thumbnails.
- New "Follow Packs" launchable column (App Drawer + Discover "Browse all")
  with multi-field search across title, description, creator name/npub,
  and `t` tags.
- Pack detail overlay (read-only) with per-member Follow/Unfollow buttons
  that reflect the live kind-3 state, plus pack-level Follow all /
  Unfollow all with a dedupe-aware confirm dialog ("Follow N new (M
  already followed)").
- Bulk follow / unfollow batched into a single kind-3 publish via new
  `FollowActions.buildUnfollowBatch` and `Kind3FollowListState.follow/
  unfollow(users: List<User>)`. The mutating call sites are Mutex-
  protected against concurrent races.
- naddr → 39089 references in notes render as a rich inline card with
  avatar stack + Follow all CTA. Cache miss triggers a one-shot
  subscription; empty / deleted packs render minimal states.
- Shuffle button rotates both the featured pack and the gallery,
  excluding the last 5 shown.
- Pack image fields render via Coil `AsyncImage` with a deterministic
  gradient fallback.

Protocol additions:
- Quartz: `FollowListEvent.hashtags()` convenience accessor.

Bug fixes wrapped into the feature:
- `DesktopLocalCache.consumeContactList` now also loads the event into
  `addressableNotes` so `Kind3FollowListState.getFollowListEvent()`
  returns the user's actual kind-3. Without this, every bulk follow
  silently replaced (rather than appended to) the contact list.
- Added Material Symbols `Shuffle` codepoint and regenerated the
  bundled subset font (still 432 KB).
2026-06-30 12:05:16 +03:00
Claude 8b4f5e28f4 fix(napplet): let users re-trigger NIP-07 connect after cancelling
Pressing Back on the "Connect to Nostr" first-connect dialog resolves to
AppConnectResult.Cancelled, which added the app's coordinate to an in-memory
`sessionCancelled` set. That set suppressed every future connect prompt for the
entire broker lifetime, so a later request — e.g. the user re-clicking "login"
in the in-app browser — was silently denied and the dialog never reappeared.
Because a Cancel persists nothing, the app also never showed up in Connected
Apps, leaving the user with nothing to clear to recover.

Replace the permanent suppression with a short, self-clearing cooldown
(`cancelledUntil` map): a Cancel suppresses re-prompts only briefly so the
burst of requests a page/napplet fires on load doesn't relaunch the dialog per
request, while a deliberate retry seconds later prompts again. The clock is
injectable for deterministic tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDcts4HzwSff4fy6oSVA6D
2026-06-30 00:16:31 +00:00
Claude 6355784c98 Merge remote-tracking branch 'origin/main' into claude/podcast-event-kinds-merge-vv24gd 2026-06-29 23:51:54 +00:00
Claude 2316e72573 fix(git): bookmark feedback, social divider, and disappearing-bar reset
- Top-bar repo bookmark toggle now switches between BookmarkAdd (with +)
  and Bookmark glyphs instead of two identical  glyphs, so the icon
  visibly changes shape (not just tint) when starred/unstarred.
- Add a thin HorizontalDivider after the ReactionsRow on the repo home.
- Code browser: opening/closing a file or changing folders swaps the
  scrollable in place, landing the new view at the top with no scroll
  delta, which left the disappearing top bar stranded at its hidden
  offset over a blank band. Expose the scaffold bar state via
  LocalDisappearingBarState and reset it to visible on each in-place view
  change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-29 23:01:16 +00:00
Claude f14ddd9e45 fix(git): import KMP Dispatchers.IO in GitRepositoryListState
The commonMain GitRepositoryListState used Dispatchers.IO without importing
the multiplatform kotlinx.coroutines.IO extension, so it resolved to the
JVM-only member and broke the iOS native compile
(:commons:compileKotlinIosSimulatorArm64). Matches BookmarkListState.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-29 22:44:14 +00:00
Claude a4bd541522 feat(git): snapshot cache, FAB, disappearing-bar fix, title subtitle, spacing
- Snapshot cache: a process-wide GitRepoSnapshotCache keyed by repo address.
  The browser ViewModel serves an already-fetched default-branch snapshot
  synchronously, so the stats render in share-to-image and don't re-fetch when
  switching screens.
- Issues/PR screens: filter chips now live inside the disappearing top bar
  (via the scaffold's belowBar slot) so they hide with it instead of leaving a
  static black band; the feed uses normal content padding.
- New issue is now an extended FAB on the Issues screen.
- Top bar shows the repo description as a single-line subtitle under the name;
  removed the duplicate description from the home body.
- Tighter spacing: home sections, code header rows (branch row / search /
  breadcrumb).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-29 20:50:07 +00:00
Claude 76ed4bc685 Merge remote-tracking branch 'origin/main' into claude/podcast-event-kinds-merge-vv24gd 2026-06-29 20:09:32 +00:00
Claude 2378d70326 Merge remote-tracking branch 'origin/main' into claude/git-repo-readme-code-tabs-e4uf6c 2026-06-29 20:09:01 +00:00
Claude a687e03461 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-wizard-0zr280
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
2026-06-29 19:26:56 +00:00
Claude 144f543014 fix(git): move browser ViewModel factory app-side for KMP lifecycle
The KMP lifecycle-viewmodel artifact used by commons doesn't expose the
create(Class<T>) ViewModelProvider.Factory override (only the desktop/JVM
target hit this), so the factory now lives in amethyst alongside the
viewModel() call, mirroring the NestViewModel pattern. The commons
ViewModel keeps only platform-agnostic state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-29 00:00:31 +00:00
Claude 64567c7056 refactor: move V4VSplitEditorState to commons for cross-front-end reuse
The value-for-value split editor's state holder is pure snapshot state over
quartz types + a commons User — no Account, LocalCache, AccountViewModel, or
Android dependency — so per the commons architecture (state holders belong in
commons, CLI-safe where practical) it moves to commons.podcasts. A future
Desktop/iOS V4V editor can now drive the same state; the editor composable stays
platform-side (it needs AccountViewModel + user search).

This is the only podcast app-layer file that's free of amethyst-only
foundations: the rest of the podcast UI / ViewModels / feed filters /
subscriptions are coupled to AccountViewModel, LocalCache, Account, the
per-user subscription framework, or Android media/upload — the same foundations
every feature in the app shares, none of which live in commons — so they stay in
amethyst (as does, for the same reason, the analogous music composer). The
podcast protocol itself was already fully shared: all 50 quartz podcast files
live in commonMain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 23:01:29 +00:00
Claude 714d202e7d refactor(git): move code-browser ViewModel + syntax highlighter to commons
Relocates the two app-agnostic pieces of the NIP-34 code browser into commons
so the desktop front end can reuse them verbatim:

- GitRepositoryBrowserViewModel (+ GitBrowseState) → commons jvmAndroid
  nip34Git package. Pure StateFlow ViewModel over quartz's GitHttpClient;
  no Android/AccountViewModel/INav dependency.
- CodeHighlighter → commons commonMain nip34Git/ui. Pulls the Apache-2.0
  dev.snipme:highlights dependency into commons commonMain.

Amethyst composables now import both from commons. The screen-level
composables stay app-side, matching the commons convention that shared UI
never takes AccountViewModel/INav.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 22:55:56 +00:00
Claude 5c29c277c8 Merge remote-tracking branch 'origin/main' into claude/git-repo-readme-code-tabs-e4uf6c 2026-06-28 22:29:16 +00:00
Claude 5aaaa90cd7 feat(git): bookmark repos + label filter & counts on status feed
Adds NIP-51 (kind 10018) repository bookmarking with a star toggle in the
git repository top bar, backed by a new GitRepositoryListState in commons.
Removal rebuilds the public tag set and re-signs so encrypted private
bookmarks are preserved without decryption.

Adds a label-filter chip row and open/closed item counts to the Issues and
Patches & PRs status feeds. The active feed's distinct labels drive the
chips; selecting one filters the rendered list, and a stale selection is
dropped when switching status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 21:57:31 +00:00
Claude 291fda5728 feat: add README and Code tabs to the git repository screen
Render the repository README in the first tab and add a Code tab that
browses the repo's file tree and renders source files (syntax-highlighted),
reading directly from the NIP-34 clone URL over the git smart-HTTP v2
protocol (works with GRASP/ngit bare servers as well as GitHub/GitLab).

quartz (jvmAndroid): a from-scratch git smart-HTTP v2 client — pkt-line
codec, packfile parser with OFS/REF delta resolution and SHA-1 oids,
tree/commit parsers, and a high-level browser that fetches a shallow
filter=blob:none snapshot (one request for the whole tree) and lazily
pulls file blobs on demand. Offline tests run against real captured
GitHub wire bytes plus a git-generated OFS-delta pack.

amethyst: README tab (rich markdown), Code tab (folders-first browser
with breadcrumb navigation + a file viewer that renders markdown or
syntax-highlighted source), a browser ViewModel, new UI strings, and a
Folder material symbol (font subset regenerated). Syntax highlighting
uses dev.snipme:highlights (Apache-2.0, permissive).

Tabs are now: README, Code, Overview, Issues, Patches & PRs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 14:46:32 +00:00
Claude 546d4ce9c8 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-wizard-0zr280 2026-06-27 23:00:26 +00:00
Claude d05247541d refactor: extract shared napplet abstractions, remove what-comments
- Extract resolveNappletMeta() to NappletManifestLookup.kt, replacing
  three private copies of the same manifest lookup across
  ConnectedAppsScreen, ConnectedAppDetailScreen, NappletPermissionsScreen,
  and NappletSignerConsentActivity.
- Extract PolicyCard composable to PolicyCard.kt, shared between
  ConnectedAppDetailScreen and RelayAuthSettingsScreen (was duplicated).
- Extract NappletCapability.symbol() to NappletCapabilityExt.kt, shared
  between ConnectedAppDetailScreen and NappletPermissionsScreen.
- Drop what-comments on kind 1/6/7 lines in NostrSignerPermissionLedger.
- Reword TrustedRelayListState stateIn comment to note private-tag absence
  on first boot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:52 +00:00
Claude 330379e53f fix(napplets): address four code-review findings before merge
- Cancel on first-connect dialog now suppresses re-prompting for the
  rest of the session (sessionCancelled set under signerConsentLock)
  instead of showing the dialog on every subsequent request.
- PARANOID signer policy no longer silently bulk-grants ALLOW_ALWAYS
  for all capabilities; the capability ledger is left empty so each
  capability prompts individually, matching user intent.
- NostrSignerOp.Decrypt default changed from ALLOW → ASK in
  reasonableDecision(); the branch is currently unreachable
  (toSignerOp() never produces Decrypt) but ASK is the safer default
  if a decrypt request type is added in future.
- TrustedRelayListState seeds its StateFlow from the synchronously
  available cached relay set, eliminating a startup window where
  IF_IN_MY_LIST incorrectly denied auth to relays in the user's list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:52 +00:00
Claude 52f1b8723c feat(napplets): require Amethyst consent for all signer types, auto-approve encrypt/decrypt
Remove the signer self-gating bypass that allowed external (Amber/NIP-55)
and remote (NIP-46) signers to skip Amethyst's per-napplet consent UI.
All signer types now go through Amethyst's consent dialogs first; the
external signer then adds its own approval on top (double-prompting).
This lets users differentiate signing requests by app inside the external
signer, since Amethyst itself is the requesting app.

Also expand the REASONABLE policy to auto-approve Encrypt and Decrypt
operations, matching the intent that common/private-key operations that
apps routinely need are pre-approved at the "reasonable" trust level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:51 +00:00
Claude 328790ac9f feat: time-bound signer grants, last-used tracking, and relay AUTH settings (NIP-42)
- 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
2026-06-27 22:58:51 +00:00
Claude b48cad67e5 feat: add Nostr signer permission system for napplets/nsites
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
2026-06-27 22:58:50 +00:00
Claude 756648aef1 feat: add HiveTalk to discover web apps
HiveTalk (hivetalk.org) — Nostr-native, Lightning-powered video
conferencing. Reachability and own apple-touch-icon verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
2026-06-27 18:44:50 +00:00
Claude 2d6221fd10 feat: show curated descriptions for discover web apps
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
2026-06-27 18:34:53 +00:00
Claude 5029acccee feat: expand suggested web apps list with icons and reachability check
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
2026-06-27 18:12:18 +00:00
Claude f206b0bb46 feat: suggest a default list of Nostr web apps in the empty browser
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
2026-06-27 17:07:18 +00:00
Claude 45de9b1b69 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-wizard-0zr280 2026-06-27 00:42:20 +00:00
Vitor PamplonaandGitHub ceca5d52ae Merge pull request #3394 from vitorpamplona/claude/composable-memory-ci-build-jxpwkv
Move UI components to commons module for better code sharing
2026-06-26 20:12:09 -04:00
Claude 94677d1677 refactor: extract self-contained layout/preview leaves to :commons
Continues moving genuinely platform-agnostic leaves out of the :amethyst app
module so they compile once in :commons instead of across all six app variants.

Moved (no Android coupling, no foundation deps):
  - ui/layouts/DisappearingBarState, DisappearingBarNestedScroll, PaddingMerge
    -> commons commonMain (com.vitorpamplona.amethyst.commons.ui.layouts)
  - ui/components/UrlPreviewState
    -> commons jvmAndroid (it references commons.preview.UrlInfoItem, which
       lives in the jvmAndroid source set)

Consumers (incl. the existing DisappearingBar*Test unit tests, which stay in
:amethyst and now import from commons) updated to the new packages. No behavior
change.

Verified: :commons, :amethyst compilePlayDebugKotlin + compilePlayDebugUnitTest,
and :desktopApp:compileKotlin build clean; spotless applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
2026-06-27 00:06:52 +00:00
Claude 8b74a3e24e refactor: extract 7 pure leaf composables from :amethyst to :commons
First Tier A slice of the UI/components extraction. Moves the genuinely
platform-agnostic leaf composables — those with zero :amethyst
dependencies and no Android coupling — from the app module into the
shared :commons KMP module (commonMain):

  ClickableTexts, ForwardingPainter, GenericLoadable, GlowingCard,
  LoadingAnimation, TranslationConfig, ZonedSwipeModifier  (~616 LOC)

These now live under com.vitorpamplona.amethyst.commons.ui.components and
compile once in :commons (cacheable, incremental) instead of being part
of every one of the six :amethyst variant compilations (play/fdroid ×
debug/release/benchmark). That shrinks the app-module Kotlin compilation
unit — the root cause of the CI Kotlin-daemon OOM — rather than renting
headroom with heap flags.

Consumers updated to import from the new package; no behavior change.
Verified: :commons, :amethyst compile{Play,Fdroid}DebugKotlin, and
:desktopApp:compileKotlin all build clean; spotless applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
2026-06-26 22:29:37 +00: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
Claude 2fddb73365 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-wizard-0zr280 2026-06-26 19:08:04 +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
Claude cde3b2b047 feat(cashu): add find-or-create wallet wizard with cross-relay discovery
The Cashu "Add wallet" screen had become a mint manager, and its Create
path published a fresh kind:17375 that could clobber a portable NIP-60
wallet the user already owned in another client (kind:17375 is
replaceable).

When no wallet is loaded, drive the user into a new find-or-create wizard
that crawls every relay (modeled on the Event Sync tool) for the user's
existing kind:17375 wallets and branches on the result:

- 0 wallets: offer to create a new one.
- 1 wallet: verify + balance-probe it, adopt as the main wallet and
  rebroadcast to outbox so it's easy to find next time.
- >1 wallets: the newest becomes the main wallet; older/duplicate wallets
  are verified, their recoverable balances probed via NUT-09/NUT-07, and
  the user gets one-tap "Recover funds to main wallet" per old wallet.

The mint manager (AddCashuWalletScreen) is now reachable only from Cashu
Wallet Settings, once a wallet exists; the no-wallet and "add wallet"
picker paths route through the wizard instead.

Implementation:
- Split CashuWalletOps.restoreFromMint into scanRecoverableProofs (no
  publish, for the balance probe) + publishRecoveredProofs; foreign-seed
  recovery never bumps the main wallet's NUT-13 counter.
- New CashuWalletDiscovery crawler reuses fetchAllPages + a fresh
  NostrClient so crawled events don't pollute LocalCache.
- CashuWalletState gains decrypt/probe/recover/adopt helpers.
- Extract AccountViewModel's relay-crawl closures (crawlRelayDb,
  buildCrawlClient) so Event Sync and the wizard share them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
2026-06-26 16:22:18 +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
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
Vitor PamplonaandClaude Opus 4.8 aa520c361f feat(embed): hybrid word+char granularity for in-field selection handles
Completes parity #5. `fieldExtend` kept a pure word-snap, so the in-field
start/end handles couldn't be fine-tuned to a single character. Now it keeps
per-drag state (reset on a >250ms gap or edge switch) and matches native
`Editor` word-selection drags: the gesture baselines at the current selection
edge, sweeping past that word's far boundary snaps to the next whole word
(never stopping mid-gap), and moving within / back from the furthest-reached
word gives character precision. Symmetric for both handles. Page-text extend
stays character-granular.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:25:55 -04:00
Vitor PamplonaandClaude Opus 4.8 dec1e0bec1 feat(embed): selection loupe + native-parity fixes for embedded text selection
Builds out host-drawn text selection for embedded napplet/nsite/browser
surfaces toward native parity, and fixes the bugs found while exercising it.

- Magnifier loupe (#4): EmbeddedMagnifier + provider-side pixel capture
  (EmbeddedMagnifierProbe) shipped over IPC for both embed paths; the
  caret/selection handles drive it via OnMagnify.
- SelectionUiState: single source of truth for the overlay show/hide rules
  (insertion caret / in-field range / page-text range + dragging/scrolling).
- EmbeddedSelectionDrag: suspends the nav drawer's edge swipe while a handle
  is dragged (auto-scroll #9).

Bug fixes:
- No more overlay blink on word-select: the shim's selection-reveal scrolls
  (a textarea auto-scrolling to show a forming/re-asserted range) no longer
  trip the hide-on-scroll path, and the hide self-heals instead of being
  re-armed indefinitely.
- RemoteImeView debounces the range-lost signal so a transient collapse that
  gets re-asserted doesn't flicker the handles/toolbar.
- Focusing a field clears any page-text selection (shim + host), so the stale
  page handles/Copy bar no longer linger above — and stop stealing drags from —
  the field overlays; also cancels any in-flight scroll-hide on focus.
- Caret insertion-handle drag now actually moves the caret: read the pointer
  delta with positionChangeIgnoreConsumed() before consuming, so the value
  isn't zeroed by our own consume (or the sandbox surface consuming the move).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:25:55 -04:00
Vitor PamplonaandClaude Opus 4.8 baddd5b3bd fix(embed): IME typing + host-drawn text selection for embedded surfaces
Embedded WebView surfaces (:napplet process, SurfaceControlViewHost) can't
host the soft keyboard or present Chrome's own selection UI, so editing and
selection are relayed to the main process. This lands the working set of that
relay:

- shim.js: fix React-controlled input erase by writing through the native
  HTMLInputElement/HTMLTextAreaElement value setter (so React's value tracker
  stays in sync); make the host authoritative for selection re-assert (the
  editable selectionchange handler only mirrors); report field/caret geometry
  and page-text selection geometry; add pageExtend + caret coords (border-width
  corrected) for drag-to-extend and the insertion handle.
- RemoteImeView: land caret where tapped on focus (requestFocus before applying
  remote state); host-authoritative selection re-assert within a time window;
  setText only when text actually changed; wire copy/cut/paste/select-all and
  edit callbacks.
- EmbeddedTabLayer: stop resizing the surface on IME show (removes the ~1s
  first-letter freeze); draw the selection overlay — toolbar, teardrop
  selection handles, and the insertion (cursor) handle.
- EmbeddedImeBridge / Embedded{Napplet,Browser}Controller: carry selection +
  caret geometry and the page-selection event across the Messenger channel.

Known limitation (not fixed here): after a field's page is opened in its own
full-screen activity and the user returns, the selection-highlight paint stays
off across all embedded surfaces. DOM selection, the toolbar, and copy still
work — only the native highlight is gone. This is a WebView/Chromium behavior
in off-window surfaces and is not reachable from the app layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:25:55 -04:00
Claude f2a7548434 fix: move shared browser/napplet strings to :commons to eliminate cross-module duplicate
AGP 9.2.1 treats resources defined in both an app module and a library module
with the same key (qualifiers="") as an error in non-debug builds.

`browser_address_hint`, `browser_console_title_short`, `browser_console_title`,
`browser_console_clear`, and `napplet_untitled` were defined in both
`:amethyst/values/strings.xml` and `:nappletHost/values/strings.xml`.
Both `:amethyst` and `:nappletHost` depend on `:commons`, so the canonical
home for these shared strings is `commons/src/androidMain/res/values/strings.xml`.

Update callers in both modules to use `com.vitorpamplona.amethyst.commons.R as
CommonsR`. Locale translations in amethyst's `values-*/` directories remain as
Android resource overlays (app module overrides library module at merge time).

Fixes: Found item String/browser_console_clear more than one time (packageFdroidBenchmarkResources)
2026-06-26 00:11:31 +00:00