Commit Graph

348 Commits

Author SHA1 Message Date
nrobi144 325a1f6f6d fix: address code review — NWC key cleanup, CancellationException rethrow
- removeAccountFromStorage now deletes nwc_<npub> keychain entry
  (fixes orphaned wallet key on account removal)
- Rethrow CancellationException in loadSavedAccount and loginWithKey
  catch blocks (fixes broken structured cancellation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 11:33:16 +03:00
nrobi144 46caa4d795 fix(desktop): harden account security — NWC to keychain, single source of truth
CRITICAL: Move NWC wallet secret from plaintext nwc_connection.txt to OS
keychain. The NWC secret is a private key that can authorize Lightning
payments — storing it in plaintext allowed any process to steal funds.

Security fixes:
- NWC secret stored in OS keychain as "nwc_<npub>" (per-account)
- accounts.json.enc is now the sole source of truth for cold boot
- Eliminate bunker_uri.txt, last_account.txt, nwc_connection.txt
- Legacy files deleted on first startup (one-time cleanup)
- logout(deleteKey=true) now removes account from accounts.json.enc
- Corrupted accounts.json.enc backed up as .corrupt.<timestamp>

Cold boot rewrite:
- loadSavedAccount() routes by SignerType from accounts.json.enc
- No longer reads stale bunker_uri.txt (fixes nsec→bunker confusion)
- No longer reads last_account.txt (uses activeNpub from metadata)

Multi-account improvements:
- NWC connections are per-account (switch account = switch wallet)
- Each account type (Internal/Remote/ViewOnly) loads correctly
- saveBunkerAccount() no longer writes to bunker_uri.txt

Updated 8 existing test files to use accountStorage instead of
writing legacy files directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 11:29:42 +03:00
Claude 3ff721b31a refactor: move packaging/appimage into desktopApp module
The AppImage build inputs (AppRun, .desktop entry, icon, and the
CI-fetched linuxdeploy binary) are consumed only by desktopApp's
createReleaseAppImage task. Co-locating them under
desktopApp/packaging/appimage/ removes the `../` path escape from the
build script and keeps all desktop packaging assets inside the module.

https://claude.ai/code/session_0137ULcfJkASmfmffFBdW8ac
2026-05-14 18:21:28 +00:00
Claude 4d2886d162 refactor(timeago): consolidate toggle into one stable composable
Audit follow-up — the toggle behaviour now lives in a single
`ToggleableTimeAgoText` core in `ui/note/elements/TimeAgo.kt`. `TimeAgo`,
`NormalTimeAgo`, `ChatTimeAgo`, and `ChatroomHeaderCompose.TimeAgo` are
thin wrappers that pick a `TimeAgoStyle` (Dotted / Short) and pass
colour/font params; no per-site duplication of state + clickable +
derivedStateOf.

Performance fixes that matter for a feed with hundreds of timestamps:

- `rememberSaveable` → `remember`. Persisting a transient peek-toggle to
  the SavedStateRegistry for every visible+scrolled-past note was pure
  memory bloat. Recycling now resets to relative, which is the expected
  behaviour for a transient inspect action.
- The relative-mode `derivedStateOf` lambda reads `nowState.value` only
  when displaying a relative time. An item the user has frozen to its
  absolute date no longer re-evaluates every 30-second tick.
- Core composable takes only stable primitive params (Long, Color,
  TextUnit, TextOverflow, enum) so Compose can skip it entirely when
  inputs don't change.
- Desktop wrapper dropped the redundant `derivedStateOf`: its formatter
  reads no State, so derivedStateOf had nothing to observe.

https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
2026-05-13 19:31:15 +00:00
Claude 38463e5f2c feat: tap TimeAgo to toggle relative ↔ absolute timestamp
Every TimeAgo composable (Android NoteCompose timestamp, NormalTimeAgo,
ChatTimeAgo, ChatroomHeaderCompose last-message time) and every Desktop
timestamp Text (NoteCard, NotificationsScreen, ChatPane, ConversationListPane)
is now clickable and toggles to a scale-adjusted absolute date/time:

  - same day → time only (e.g. "14:32"), locale-aware
  - same year → "MMM dd, HH:mm"
  - older    → "MMM dd, yyyy"

State is hoisted per-call site via rememberSaveable so the toggle survives
scroll-induced disposal in lazy lists. Desktop call sites share a small
ToggleableTimeAgoText wrapper; Android keeps its existing composable shapes
and just gains a clickable modifier + state.

https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
2026-05-13 19:17:55 +00:00
Vitor Pamplona 5c39e3c85b Removes more warnings 2026-05-12 22:05:38 -04:00
nrobi144 2b96e83264 feat(desktop): add embedded local relay with SQLite event persistence
Add an in-process local relay to Amethyst Desktop that persists all
received events to a per-account SQLite database using quartz's existing
EventStore infrastructure. On startup, the local store hydrates
DesktopLocalCache for instant feed rendering before remote relays connect.

- LocalRelayStore: manages per-account EventStore lifecycle, batched
  write-through via BasicBundledInsert (250ms window), startup hydration
  (contact list -> metadata -> recent content events)
- LocalRelayMaintenance: periodic cleanup (NIP-40 expiration, 30-day
  prune, weekly VACUUM), disk space monitoring
- Settings UI: integrated into RelaySettingsScreen with statistics,
  storage management (prune/vacuum/clear), JSONL export/import, and
  error display
- OfflineBanner: animated banner in both SinglePaneLayout and
  DeckColumnContainer showing offline status with local cache indicator
- Thread-safe store access via @Volatile + synchronized lock
- Skips re-enqueue during hydration (checks LOCAL_RELAY_URL)
- DB stored at ~/.amethyst/accounts/<pubkey8>/events.db
- Corrupt DB auto-detected and recreated on open

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 11:35:39 +03:00
m 3383b60315 feat(namecoin): distinguish malformed-JSON values from missing-field
When a Namecoin record's value isn't valid JSON (a real failure mode
when an operator hand-builds the value and miscounts braces), the
NIP-05 path used to silently swallow the parser exception and surface
a misleading "no nostr field" message. That sends the publisher
chasing a phantom missing field when the actual problem is the value
itself.

Concrete case that triggered this: a `name_update` published a
474-byte d/testls value with one closing brace short of balanced. The
string parses up to the missing brace, after which kotlinx.serialization
throws "Unfinished JSON term at EOF at line 1, column 474". That error
was previously dropped, leaving the operator to debug "no nostr field"
without ever seeing the underlying JSON parse failure.

Changes:

- New NamecoinResolveOutcome.MalformedRecord(name, error). Distinct
  from NoNostrField. The `error` field is the parser's own diagnostic
  (e.g. "Unfinished JSON term at EOF at line 1, column 474") so the
  publisher can locate the broken byte without spelunking.
- NamecoinNameResolver.performLookupDetailed: parse via a new
  parseValueOrError helper and surface MalformedRecord instead of
  collapsing into NoNostrField. Also rejects non-object top-level
  values (arrays, primitives, null) with a useful diagnostic
  ("top-level value is JsonArray, expected JSON object").
- DesktopSearchScreen handles the new outcome by surfacing the parser
  error verbatim in the Namecoin status banner, so the column number
  reaches the publisher's screen.

Tests (commonTest / NamecoinImportTest):

- "NIP-05 lookup surfaces MalformedRecord with parser detail when
  value is broken JSON": a deliberately one-brace-short value yields
  MalformedRecord with a non-empty diagnostic.
- "NIP-05 lookup surfaces MalformedRecord when top-level value is a
  JSON array": ensures non-object top-level values are rejected with
  a useful "expected JSON object" message rather than silently
  parsing as something unusable.

Tests don't pin the exact parser wording (kotlinx.serialization can
change it across versions); they only pin that the message is
attributed to JSON parsing rather than to a missing field.
2026-05-09 10:02:26 +10:00
Vitor Pamplona abd565a460 Merge pull request #1914 from mstrofnone/desktop-namecoin-port
Port Namecoin NIP-05 resolution to Desktop app
2026-05-08 08:55:09 -04:00
m 9d84d01569 style: spotlessApply formatting + fix icon imports for upstream MaterialSymbols 2026-05-08 22:38:39 +10:00
m 4ab9cae213 fix(desktop): provide LocalNamecoin{Preferences,Service} so search can resolve
The lazy-init declarations and CompositionLocalProvider entries were lost
during the rebase, so SearchScreen always saw null services and skipped
Namecoin resolution. Add them back inside the LoggedIn branch alongside
LocalTorState.
2026-05-08 22:33:06 +10:00
m 50d0e15fe2 fix(desktop): show Namecoin lookup results in search
Re-add the Namecoin results UI block that was lost during the rebase.
Renders Loading/Resolved/NotFound/Error states above bech32/people/note
results when the query is a Namecoin identifier (.bit, d/, id/).
2026-05-08 22:31:46 +10:00
M dca55ebcc4 fix: update renamed APIs (trustAllCerts→usePinnedTrustStore, IRequestListener→SubscriptionListener)
Adapt cherry-picked PR commits to current main where:
- ElectrumxServer.trustAllCerts was renamed to usePinnedTrustStore
- IRequestListener was renamed to SubscriptionListener
2026-05-08 22:31:33 +10:00
M 645bcf8f44 refactor: lazy-load Namecoin services on Desktop (not kept in memory from start)
Move Namecoin service initialization from App-level (eager, on every startup)
to inside the LoggedIn branch (only created when user logs in and screens
that use Namecoin are reachable). Matches the Android lazy pattern in AppModules.

Also deduplicate NamecoinSettings: Android module now uses a typealias to the
commons module version, matching the original PR intent.
2026-05-08 22:31:33 +10:00
M 5369694b4d refactor: move NamecoinSettings and NamecoinResolveState to commons module
Eliminates code duplication between Android and Desktop:

- Move NamecoinSettings to commons/model/nip05DnsIdentifiers/namecoin/
  (with @Serializable and @Stable annotations)
- Extract NamecoinResolveState sealed class to its own file in commons
- Move NamecoinSettingsTest to commons (shared by both platforms)
- Replace Android NamecoinSettings.kt with a typealias to commons
- Update all imports in Desktop and Android modules
- Remove debug println statements from ImportFollowListDialog and Main
2026-05-08 22:31:33 +10:00
M 0b636d62aa Fix follow list import: validate pubkey length + add crash monitoring
Root cause: decodePublicKeyAsHexOrNull() returns truncated hex for
non-bech32 input (its else branch runs Hex.decode on arbitrary strings).
The relay then rejects the filter with 'Invalid author length'.

Fixes:
- Check raw 64-char hex FIRST (before bech32 parsing)
- Only attempt bech32 decode for strings starting with npub1/nprofile1/nsec1
- Validate all resolved pubkeys are exactly 64 hex chars
- Show specific error messages per identifier type instead of generic fallback
- Improved debug logging with pubkey prefix for each resolution path

Crash monitoring:
- Added Thread.setDefaultUncaughtExceptionHandler in Main.kt
- Logs crashes to ~/.amethyst-desktop-crash.log with timestamp and full stack trace
- Also prints to stderr for Gradle console visibility
2026-05-08 22:31:33 +10:00
M 0bc1c2838d desktop: full relay-integrated Import Follow List dialog
Rewrites ImportFollowListDialog with complete relay integration:

- Resolves identifiers: npub, hex, NIP-05 HTTP (via OkHttp), Namecoin
- Fetches kind 3 (ContactListEvent) from relays via subscription
- Parses follow list (p-tags) into selectable entries
- Fetches kind 0 metadata for display names (best-effort)
- Shows preview with select all/deselect all + individual toggles
- Publishes new kind 3 via broadcastToAll with selected follows
- State machine: Idle → Resolving → Fetching → Loaded → Publishing → Done
- Proper cleanup: DisposableEffect unsubscribes on dismiss
- 15s timeout for kind 3, 20s timeout for metadata enrichment
- Updates Main.kt call site to pass relayManager, account, localCache
2026-05-08 22:31:33 +10:00
M 21133c34c0 Fix search: use resolveDetailed() for proper timeout handling
The search bar was using resolve() which lets
NamecoinLookupException.ServersUnreachable propagate as an exception,
causing 'servers unreachable' to appear immediately instead of waiting
for the actual lookup to complete.

Switch to resolveDetailed() which catches exceptions internally and
returns typed outcomes (Success/NameNotFound/NoNostrField/
ServersUnreachable/InvalidIdentifier/Timeout). The search screen now
shows Loading for the full duration of the attempt (up to 20s) and
only shows the error after all servers have actually been tried.
2026-05-08 22:31:12 +10:00
M a7c4842d60 Wire ImportFollowListDialog into File menu (⇧⌘I)
The dialog was created but never accessible from the UI. Now:
- Added 'Import Follow List…' menu item in File menu (Shift+Cmd+I / Shift+Ctrl+I)
- showImportFollowListDialog state threaded through App composable
- Dialog rendered alongside ComposeNoteDialog and AddColumnDialog
2026-05-08 22:30:55 +10:00
M 5602429f9b Port Namecoin NIP-05 resolution to Desktop app
Add censorship-resistant NIP-05 verification using the Namecoin blockchain
to the Desktop (JVM) app, porting functionality from PRs #1734, #1771,

New files:
- DesktopNamecoinNameService: app-level service wrapping the Quartz
  NamecoinNameResolver with caching, custom server support, and live
  state flows. Uses plain JVM sockets (no Tor support on Desktop yet).
- DesktopNamecoinPreferences: Java Preferences API-backed persistence
  for Namecoin settings (enabled toggle + custom ElectrumX servers).
- NamecoinSettings: Desktop copy of the settings data class (no Android
  dependencies).
- LocalNamecoin: CompositionLocals for threading Namecoin service/prefs
  through the compose tree.
- NamecoinSettingsSection: Compose Desktop UI for configuring ElectrumX
  servers — toggle, active server display with DEFAULT/CUSTOM badge,
  add/remove custom servers, reset to defaults. Uses onPreviewKeyEvent
  for Enter-to-submit instead of Android KeyboardActions.
- ImportFollowListDialog: Dialog for importing follow lists via npub,
  hex, NIP-05, or Namecoin identifiers. Resolves identifiers to pubkeys
  with Namecoin blockchain support.

Modified:
- Main.kt: Instantiate DesktopNamecoinPreferences and
  DesktopNamecoinNameService in App composable, provide via
  CompositionLocals, wire into RelaySettingsScreen.
- SearchScreen.kt: Detect Namecoin identifiers (.bit, d/, id/) in the
  search bar, resolve via DesktopNamecoinNameService with loading/error
  states, display resolved user above standard results. Uses
  LaunchedEffect with key-based cancellation for stale lookups.
- DeckColumnContainer.kt: Pass namecoinPreferences to RelaySettingsScreen
  from CompositionLocal.

Tests:
- DesktopNamecoinPreferencesTest: round-trip persistence, add/remove
  servers, enable/disable, reset, duplicate handling.
- NamecoinSettingsTest: server string parsing/formatting, round-trips,
  edge cases, toElectrumxServers conversion.

The Quartz KMP library (commonMain + jvmAndroid) already contains the
core Namecoin resolution code (ElectrumXClient, NamecoinNameResolver,
NamecoinLookupCache) shared across Android and Desktop.
2026-05-08 22:29:23 +10:00
m 01f6c1c24b fix(desktop): make the single-pane navigation rail scrollable
The left navigation rail in single-pane mode renders a fixed list of
pinned screens (Home, Reads, Notifications, ...) plus a 'More' launcher
and a stack of bottom controls (relay health, bunker heartbeat, tor
status, account switcher).

Material3 `NavigationRail` lays its children out in a non-scrollable
`Column`. When the window is short — either because the OS window is
small or the user pinned several screens — the bottom items in the
list (and the 'More' button) get clipped and become unreachable.

Replace the `NavigationRail` with a `Column` that mirrors the rail's
container styling and splits content into two regions:

- A scrollable region (weight(1f) + verticalScroll) holding the pinned
  screens and the 'More' launcher. Overflow now scrolls instead of
  clipping.
- A fixed bottom region holding the relay health indicator, bunker
  heartbeat, tor status indicator, and the account switcher. These
  remain anchored at the bottom of the rail.

Item visuals are preserved by keeping `NavigationRailItem` for the
items themselves, with `NavigationRailItemDefaults.colors()`.

No behavior change when the rail content already fits the window.
2026-05-08 22:28:17 +10:00
Vitor Pamplona 317b205858 Merge pull request #2781 from mstrofnone/fix/selectable-error-messages
fix(desktop): make error messages selectable so users can copy them
2026-05-08 08:12:49 -04:00
Vitor Pamplona 0127ed4412 Merge pull request #2778 from nrobi144/fix/bunker-timeout-and-decrypt
feat(desktop): custom feeds system with creation, discovery, and author search
2026-05-08 08:11:55 -04:00
m 3e246e9e0b fix(desktop): make error messages selectable so users can copy them
Several user-visible error messages on Desktop are rendered with plain
`Text` composables, which means they can't be selected or copied. That
makes it awkward to share an error in a bug report or paste a hex error
string into a search.

Wrap the error text in `SelectionContainer` at the canonical sites:

- `commons.ui.components.LoadingState`: wrap the description in
  `EmptyState` and the message in `ErrorState`. `EmptyState` is reused
  as the in-feed error renderer (e.g. 'Error loading feed' with the
  underlying error in `description`), so this covers feed/loading
  errors across screens that use these helpers.
- `ComposeNoteDialog`: wrap the validation error and the upload error
  in the compose-note dialog.
- `auth/LoginCard` (Nostr Connect): wrap the connection error.
- `auth/KeyInputField`: wrap the supporting-text error so the inline
  message under the nsec input field can be copied.

No visual changes \u2014 `SelectionContainer` does not affect layout or
styling. Selection works on Compose Desktop (mouse drag) and on Android
(long-press) without further changes.
2026-05-08 16:32:48 +10:00
nrobi144 de879b9472 fix(desktop): use SearchBarState + rememberSubscription for author search
Replace manual relay subscribe + Channel approach with the proven
SearchBarState + rememberSubscription pattern (same as NewDmDialog).
This properly handles relay connection lifecycle and NIP-50 search,
returning results from all connected relays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 07:37:27 +03:00
nrobi144 41920a363a feat(desktop): author search in feed builder with relay NIP-50 + avatars
- Hoist author search state to FeedsDrawerTab (AlertDialog can't run LaunchedEffect)
- NIP-50 relay search via connected relays with Channel-based result streaming
- 28dp UserAvatar in author suggestion rows
- "No users found" empty state
- 32dp dialog margins, 300dp max results height, 30 result limit
- FindUsersTest: 8 tests verifying cache search with/without metadata
- Fix: use connectedRelays instead of unconnected DEFAULT_SEARCH_RELAYS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 07:23:44 +03:00
davotoula a2d3c478d2 Add pendingPublishRelaysFor stub to desktopApp StubNostrClient
Add pendingPublishRelaysFor stub to test NostrClient fakes
2026-05-07 21:57:39 +02:00
Claude d2294247a7 fix(desktop): pin vlcVersion to 3.0.20 so Linux vlcDownload finds an artifact
The Linux build path of the vlc-setup plugin pulls vlc-plugins-linux from
Maven Central (ir.mahozad:vlc-plugins-linux), which only ships 3.0.20 and
3.0.20-2 — there is no 3.0.21 artifact yet. With vlcVersion set to 3.0.21,
both the CI pre-fetch step and the in-Gradle vlcDownload task hit a 404.

Match VLC_VERSION in build.yml and document the lag so future bumps wait
for the Maven artifact to catch up.

https://claude.ai/code/session_01GrZLMi3sdp6frwREmQ9cUi
2026-05-06 23:06:52 +00:00
nrobi144 447f89d2c1 feat(desktop): add UserSearchEngine + CompositionLocal providers for cache/relay
- UserSearchEngine in commons (reusable, platform-agnostic) with RelayUserSearchDelegate interface
- DesktopRelayUserSearchDelegate using direct relay manager subscribe
- LocalDesktopCache + LocalRelayManager composition locals
- Provide cache/relay at App() level so AppDrawer and dialogs can access them
- FeedsDrawerTab reads LocalDesktopCache for author display names

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 10:40:11 +03:00
nrobi144 4010a57e68 feat(desktop): add custom feeds system with creation, pinning, and inline switching
- FeedDefinition data model with FeedSource sealed interface (Filter, Following, Global, DVM, PeopleList, InterestSet, SingleRelay)
- FeedDefinitionRepository with StateFlow, groupedFeeds, pin/unpin/reorder (max 3 pinned)
- JSON serialization via Jackson with round-trip tests (18 unit tests)
- SearchQuery.toFeedDefinition() bridge for creating feeds from search
- FeedBuilderDialog with author search (cache lookup + npub decode), hashtag/relay inputs, kind filter checkboxes, exclude authors/keywords, Cmd+S save, Enter to add
- FeedsDrawerTab in app drawer with edit/delete/pin/unpin actions
- Feeds tab in top header (FilterChips) with inline mode switching
- CustomFeedScreen + DesktopCustomFeedFilter + createCustomFeedSubscription for relay-based custom feed content
- FeedDefinitionEvent (kind 31890) in quartz for future publish/import
- Local persistence via java.util.prefs.Preferences (auto-save on change)
- DeckColumnType.CustomFeed for navigation integration
- Renamed Home to Feeds in nav rail
- Fix: AnimatedGifImage race condition (coerceIn on frame index)
- Fix: search results margins (sidePadding applied consistently)
- Fix: app drawer search includes feeds in results

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 09:59:30 +03:00
Vitor Pamplona 0210d608b5 Merge pull request #2719 from nrobi144/fix/bunker-timeout-and-decrypt
fix(nip46): fix bunker decrypt/encrypt parsing and increase timeout
2026-05-04 08:09:33 -04:00
nrobi144 bc2267d79a fix(nip46): fix bunker decrypt/encrypt response parsing and increase timeout
Two issues fixed:

1. BunkerResponseDeserializer produces generic BunkerResponse for decrypt/encrypt
   results (plain strings that aren't pubkeys or JSON). The response parsers only
   matched typed subtypes (BunkerResponseDecrypt/Encrypt), causing all decrypt and
   encrypt operations through NIP-46 bunker to fail with ReceivedButCouldNotPerform.
   Fix: parsers now fall back to extracting response.result directly.

2. RemoteSignerManager timeout was 30s, shorter than Amber's 60s approval window.
   Fix: increased to 65s with safe retry (republishes same request, preserving UUID
   so bunker responses always match).

3. Desktop ChatPane showed raw ciphertext on decrypt failure instead of an error
   message. Fix: shows "Could not decrypt the message" in italic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 10:24:05 +03:00
nrobi144 fd34105439 merge: resolve conflicts with upstream/main
Merge upstream main into feat/desktop-multi-account, resolving:
- Main.kt: Surface wrapper + CompositionLocalProvider + nip11Fetcher from upstream, multi-account DeckSidebar/LoginScreen from HEAD
- FeedScreen.kt: viewport-aware scroll from HEAD + contentPadding from upstream
- DeckSidebar.kt: merged imports (multi-account + titleBarInsetTop + MaterialSymbols)
- Migrated AccountSwitcherDropdown + AddAccountDialog from material.icons to MaterialSymbols

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 07:29:20 +03:00
davotoula 3dc79ac6a5 Code review:
rethrow CancellationException; offload NWC verify off WS thread
align verification API with Amethyst Android
2026-05-02 18:42:09 +02:00
davotoula 68fb6fb703 desktop: quartz enhancements 2026-05-02 18:42:09 +02:00
nrobi144 f698b31eee fix(multi-account): account removal now switches or logs out properly
removeAccountFromStorage was calling loadSavedAccount() which reads
from legacy last_account.txt — didn't trigger feed/relay reload.

Now calls switchAccount(nextNpub) for proper relay reconnection and
feed reload, or logout(deleteKey=true) if no accounts remain.

Also cleans up bunker ephemeral keys on removal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:12:40 +03:00
nrobi144 ec3a4adb46 refactor(multi-account): in-app overlay dialog for Add Account
Replaced DialogWindow (OS native window with title bar, minimize/maximize)
with Dialog composable (in-app overlay with scrim). Matches the
ComposeNoteDialog pattern used elsewhere in the desktop app.

- Card wrapper with 480dp width, 24dp padding
- Title row with 'Add Account' + Close (✕) icon
- No OS window chrome — clean in-app feel
- Click outside or ✕ to dismiss

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:00:43 +03:00
nrobi144 09cb08e5c7 perf(cache): O(1) author lookup for metadata invalidation
consumeMetadata() was looping ALL notes in cache (O(N)) to find notes
by the author whose metadata changed. With hundreds of cached notes,
this caused lag on every metadata event.

Added notesByAuthor index (ConcurrentHashMap<HexKey, MutableSet<Note>>)
populated at loadEvent time. consumeMetadata now looks up notes by
author in O(1) instead of scanning the entire cache.

Before: ~500 notes × per metadata event = lag
After: ~3 notes per author × per metadata event = instant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 09:50:12 +03:00
nrobi144 9a586de6c7 fix(feed): key toNoteDisplayData on metadataState for reactive name updates
Root cause: toNoteDisplayData(localCache) wasn't keyed on metadataState,
so Compose could skip recomputing it when metadata arrived for items
that were composed before metadata was available (first ~2 items).

Fix: remember(event, metadataState) { event.toNoteDisplayData(localCache) }
ensures display data is recomputed when the note's metadata flow
invalidates. Applied to both regular and repost note paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 09:39:52 +03:00
nrobi144 22e50cc852 perf(feed): viewport-aware metadata loading with batched author filter
Two optimizations for feed metadata loading speed:

1. Batched author filter (FeedMetadataCoordinator.loadMetadataBatched):
   Single Filter(kind:0, authors:[all visible]) instead of individual
   per-pubkey subscriptions through rate limiter. Closes after EOSE.
   Follows ChessRelayFetchHelper one-shot pattern. Max 100 authors/filter.

2. Viewport-aware scroll observation (FeedScreen):
   snapshotFlow reads LazyListState.visibleItemsInfo (zero recomposition)
   with 500ms debounce + distinctUntilChanged. Only fetches metadata for
   visible notes ± 10 item buffer. Initial load batches first 30 notes.

Before: 100+ authors × 20/sec rate limit × 7 relays = 5+ seconds
After: ~20 visible authors × 1 batched sub × 7 relays = <1 second

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 07:15:27 +03:00
nrobi144 a826918293 fix(feed): observe metadata flow on regular notes for display name updates
Pre-existing bug: regular (non-repost) notes in the feed never observed
note.flow().metadata.stateFlow. When metadata arrived from relays and
invalidateData() was called, the composable didn't recompose — author
names and avatars stayed as hex fallbacks.

The repost path already observed metadata correctly (line 130). Added
the same observation to the regular note path (line 211).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 07:15:22 +03:00
mstrofnone fd7ce48964 ci(desktop): retry vlcDownload on transient network failures
Every desktop packaging job (Windows MSI, macOS DMG, Linux DEB) currently
breaks on the smallest blip when fetching VLC from get.videolan.org. The
`ir.mahozad.vlc-setup` plugin registers `vlcDownload` / `upxDownload`
tasks that extend `de.undercouch.gradle.tasks.download.Download`, but it
does not configure retries or a generous read timeout, so a single
`SocketTimeoutException` aborts the build.

Recent main runs all failed at the same step:

    > Task :desktopApp:vlcDownload FAILED
    > A failure occurred while executing ...DefaultWorkAction
       > java.net.SocketTimeoutException: Read timed out

Configure all `Download` tasks in this project with:

  - 5 attempts total (initial + 4 retries),
  - 30 s connect timeout,
  - 5 min read timeout (VLC archives are 40-90 MB and the mirror can be
    slow under load),
  - `tempAndMove` so a partial download from one attempt cannot poison
    the next.

This is a CI-only change \u2014 it does not alter what gets bundled, just
makes the fetch resilient.
2026-04-29 20:21:18 +10:00
nrobi144 4b1d468ed0 fix(multi-account): fetch metadata for all accounts, persist display names
Display names weren't showing because metadata was never requested for
account pubkeys. Now:

1. LaunchedEffect triggers loadMetadataForPubkeys() for all accounts
   in the switcher once relays connect
2. metadataVersion collector persists display names for ALL accounts
   (not just active one) when metadata arrives from relays
3. Names available immediately on next dropdown open

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:18:13 +03:00
nrobi144 f4308ac56d fix(multi-account): saveCurrentAccount now works for all account types
saveCurrentAccount() previously returned failure for read-only and
bunker accounts, preventing them from being added to multi-account
storage. Now:
- Read-only (npub): skips private key save, still saves to storage
- Bunker: saves to storage (private key already saved during login)
- Internal (nsec): saves private key + storage (unchanged)

This was the root cause of npub-only accounts not appearing in the
switcher and accounts being lost when adding via AddAccountDialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:09:59 +03:00
nrobi144 4c832c008c perf(multi-account): cache metadata and encryption key in memory
DesktopAccountStorage now caches AccountMetadata in memory after first
read. Subsequent loadAccounts/saveAccount/setCurrentAccount serve from
cache, only hitting disk on writes. Also caches AES encryption key to
avoid repeated OS keychain lookups.

Before: 3 decrypts + 1 encrypt + 4 keychain calls per login/switch
After: 1 decrypt + 1 keychain call on first access, then memory only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:07:56 +03:00
nrobi144 5a1b9d445c fix(multi-account): sequential account save in AddAccountDialog, debounced metadata
Root cause of accounts not persisting:
ensureCurrentAccountInStorage() was fire-and-forget (scope.launch),
so loginWithKey() ran before the old account was saved. The old account
was lost. Now all steps run sequentially in one coroutine.

UI hanging fix:
metadataVersion LaunchedEffect fired on every metadata event, doing
encrypted file I/O each time. Now debounced with 2s delay so it only
writes once after a batch of metadata settles.

Also:
- loadInternalAccount falls back to read-only (test updated)
- bunker/nostrconnect paths also await ensureCurrentAccountInStorage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:04:23 +03:00
nrobi144 156391ec0a fix(multi-account): persist display names in encrypted storage, fix npub-only fallback
Three fixes:

1. Display names now stored in AccountInfo/AccountInfoDto and persisted
   in encrypted storage. Populated when metadata arrives from relays via
   metadataVersion LaunchedEffect. Available immediately on next open.

2. loadInternalAccount falls back to loadReadOnlyAccount when no private
   key found in SecureKeyStorage — fixes npub-only accounts that were
   saved as SignerType.Internal from earlier sessions.

3. Dropdown uses persisted displayName first, cache lookup as fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 09:54:08 +03:00
nrobi144 1032db0c05 fix(multi-account): npub-only login stores ViewOnly type, reactive display names
Root cause of npub-only switch failure:
loginWithKey() with pubkey created AccountState with signerType=Internal
(default). switchAccount then called loadInternalAccount which requires
a private key from SecureKeyStorage → failed. Now sets ViewOnly.

Display names not showing:
- resolveDisplayName() ran at composition time but metadata hadn't
  loaded from relays yet. Dropdown never recomposed when it arrived.
- Added metadataVersion counter to DesktopLocalCache, incremented on
  each consumeMetadata(). Dropdown collects it to trigger recomposition
  when user names become available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 09:48:16 +03:00
nrobi144 29aa1d6c9c fix(multi-account): npub-only switching + reactive display names
- Add loadReadOnlyAccount() for ViewOnly signer type switching
  (was mapping to loadInternalAccount which requires private key)
- Remove remember() from display name resolution — now re-evaluates
  each recomposition so names appear once metadata loads from relays
- Only show subtitle (npub row) when display name is available;
  if no display name, show npub as title only
- Signer type badge shown as subtitle when no display name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 09:41:51 +03:00
nrobi144 250bb5a1ad feat(multi-account): display names, middle-truncated npub, npub-only account fix
Account switcher dropdown improvements:
- Two-row display: Display Name on top, npub (middle-truncated) below
  e.g. 'Alice' / 'npub1abc...wxyz · Bunker'
- Middle-truncation for npub: shows first 10 + last 6 chars
- Resolves display names from DesktopLocalCache user metadata
- Confirmation dialog also shows display name
- npub-only (view-only) accounts now persist to encrypted storage
  (ensureCurrentAccountInStorage called in onLoginSuccess)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 09:35:33 +03:00