Commit Graph

269 Commits

Author SHA1 Message Date
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
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
nrobi144 66310ca336 fix(multi-account): reset lastContactListCreatedAt on cache clear
Root cause of 'follows at 0 after 2+ switches':

DesktopLocalCache.clear() reset _followedUsers to empty but did NOT
reset lastContactListCreatedAt. On re-subscribe after account switch,
the contact list event arrived with the same timestamp, was rejected
by the 'event.createdAt <= lastContactListCreatedAt' guard, and
followedUsers stayed empty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 15:00:21 +03:00
nrobi144 c6b2618fd6 fix(multi-account): clear subscriptions and cache on account switch
When switching accounts, the feed showed stale data from the previous
account because subscriptions and LocalCache weren't cleared.

Now tracks previousAccountPubKey and when it changes:
1. Clears all relay subscriptions (subscriptionsCoordinator.clear())
2. Clears local event cache (localCache.clear())
3. Restarts subscriptions coordinator

Feed composables then resubscribe with the new account's pubkey
via their existing rememberSubscription(account, ...) keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 14:23:15 +03:00
nrobi144 0fb7c659f2 fix(multi-account): persist loaded account to encrypted storage on startup
loadSavedAccount() reads from legacy files (last_account.txt) but never
wrote to the encrypted multi-account storage, so the account switcher
dropdown was always empty after restart.

Now calls ensureCurrentAccountInStorage() + refreshAccountList() after
successful load so the current account appears in the dropdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 14:19:04 +03:00
nrobi144 573b478bbc fix(multi-account): preserve existing account when adding new one
- ensureCurrentAccountInStorage(): saves current account metadata to
  encrypted storage before switching to new account via AddAccountDialog
- AddAccountDialog calls ensureCurrentAccountInStorage() before each
  login method to prevent losing the previous account
- LoginScreen onLoginSuccess now calls refreshAccountList() so the
  switcher dropdown picks up the initial account immediately

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 14:16:47 +03:00
nrobi144 b1098368a1 feat(multi-account): add account switcher to single-pane layout, scrollable dropdown
- Account switcher now appears at bottom of NavigationRail in single-pane mode
  (after tor indicator), matching placement in deck sidebar
- AddAccountDialog wired in single-pane mode too
- Dropdown scrollable with 400dp max height for many accounts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 13:12:55 +03:00
nrobi144 82ec892296 feat(multi-account): add AddAccountDialog and per-account logout
- AddAccountDialog: DialogWindow (480×600) wrapping existing LoginCard
  with back button, "Import Account" title. Supports nsec, npub, bunker,
  nostrconnect. New account becomes active immediately after login.
- Per-account logout: ✕ button on each account row in dropdown, with
  AlertDialog confirmation before removal.
- Wired both into DeckSidebar and Main.kt.

Matches Android's AddAccountDialog pattern (full-screen dialog with
LoginOrSignupScreen) adapted for desktop (DialogWindow).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 12:26:00 +03:00
nrobi144 0da51d5ae3 refactor(multi-account): remove legacy migration code
Migration from single-account files didn't work reliably.
Removed migrateFromLegacyFiles() and migrateAndLoadAccounts().
Account list now populated only via saveCurrentAccount/saveBunkerAccount
when user logs in — no automatic migration from old format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 11:18:06 +03:00
nrobi144 43b6d80ea6 fix(multi-account): address review findings — per-account bunker keys, resource cleanup, simplifications
Fixes from code-simplicity and architecture reviews:

Critical fixes:
- Per-account bunker ephemeral key alias (bunker_ephemeral_<npub>) instead of
  shared alias. Prevents wrong-identity handshake when switching bunker accounts.
  Falls back to legacy alias for existing single-account setups.
- Resource cleanup in switchAccount(): closes NIP-46 subscription, stops
  heartbeat, disconnects NIP-46 client before transitioning to new account.
- Manages signer connection state on switch (Connected for bunker, NotRemote otherwise).

Simplifications:
- Removed unused createWithStorage() factory (zero callers)
- Removed loadViewOnlyAccount() dead code (no UI path to create view-only accounts)
- Removed unused allAccounts collection at Window scope (duplicated in MainContent)
- Files.move with REPLACE_EXISTING for cross-platform atomic rename
- Remote signer with empty bunkerUri falls back to Internal (prevents corrupt state)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:36:10 +03:00
nrobi144 0a378c7c4a feat(multi-account): add account switcher dropdown in sidebar
Phase 4: account switcher UI.

- AccountSwitcherDropdown composable (person icon → dropdown menu)
- Shows all accounts with active checkmark and signer type label
- "Add Account" button at bottom with divider
- DropdownMenu with DpOffset(48dp) to expand right of sidebar
- DeckSidebar now takes account params (activeNpub, allAccounts, callbacks)
- Replaces "A" logo text with account switcher button
- Migration called on startup to populate account list
- Account switch wired through to AccountManager.switchAccount()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:20:10 +03:00
nrobi144 9c6502b089 feat(multi-account): add account switching, storage, and migration to AccountManager
Phase 3: wire multi-account lifecycle into desktop AccountManager.

- Add DesktopAccountStorage field and allAccounts StateFlow
- switchAccount(): loads new account BEFORE cancelling old state
  (prevents unrecoverable partial failure per coroutines review)
- addAccountToStorage / removeAccountFromStorage for CRUD
- loadViewOnlyAccount() for npub-only accounts
- migrateAndLoadAccounts() for legacy file migration on startup
- saveCurrentAccount / saveBunkerAccount now save to encrypted storage
- Updated test mock setup to support SecureKeyStorage key store

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:15:50 +03:00
nrobi144 197db2361d feat(multi-account): add DesktopAccountStorage with AES-256-GCM encryption
Phase 2: encrypted persistence for multi-account metadata.

- DesktopAccountStorage implements AccountStorage interface
- AES-256-GCM encryption with random key stored in OS keychain
- Jackson DTOs for flat serialization (no polymorphic complexity)
- Migration helper from legacy single-account files
- Atomic writes via temp file + rename
- POSIX file permissions (owner-only read/write)
- 11 unit tests covering roundtrip, encryption, deletion, migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 07:34:07 +03:00
nrobi144 aa33b7d271 feat(multi-account): extract SignerType, AccountInfo, AccountStorage to commons
Phase 1 of multi-account support. Move account types to commons/commonMain
so both Android and Desktop share the same data model:

- SignerType sealed class (Internal, Remote, ViewOnly)
- AccountInfo data class (npub, signerType, isTransient)
- AccountStorage interface (loadAccounts, saveAccount, etc.)

Desktop AccountManager now imports SignerType from commons instead of
defining its own. All existing tests updated with new import path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 07:29:52 +03:00
Vitor Pamplona 368928e59f Merge pull request #2445 from nrobi144/feat/desktop-app-drawer
feat(desktop): App Drawer with workspaces, customizable nav bar
2026-04-19 10:57:23 -04:00
nrobi144 cb306b5219 fix(desktop): icon picker shows selected state with background highlight
Replace tint-only selection with Surface background + primaryContainer
color so the selected icon is clearly visible in the workspace editor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 05:55:09 +03:00
nrobi144 486ff2e8cf fix(desktop): UI polish — single-line layout toggle, checkmark margin
- Shorten "Single Pane" to "Single" in SegmentedButton to prevent wrapping
- Add 8dp left margin before checkmark in workspace card

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 05:52:02 +03:00
nrobi144 200c5227fc refactor(desktop): pin/unpin syncs to active workspace
PinnedNavBarState now takes WorkspaceManager reference. Pin/unpin
actions update the active workspace's singlePaneScreens list,
making sidebar customization and workspace editing the same action.

- PinnedNavBarState.syncToWorkspace() updates active workspace on pin/unpin
- PinnedNavBarState.loadFromWorkspace() loads from active workspace
- Remove separate DesktopPreferences.pinnedNavItems persistence
- Workspace is the single source of truth for nav bar screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 05:40:28 +03:00
nrobi144 0455d1f8c6 feat(desktop): single-pane workspaces store nav bar screen list
Change Workspace.singlePaneScreen (single string) to
singlePaneScreens (list of typeKey strings). First screen is
the default. On workspace switch, loads the screen list into
PinnedNavBarState and navigates to the first screen.

- Add SinglePaneScreensEditor to workspace editor dialog
- Add PinnedNavBarState.loadFromList() for workspace-driven nav
- Backward compat: load old "singlePaneScreen" format as single-item list
- Cmd+Shift+S captures current deck columns as singlePaneScreens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 05:21:12 +03:00
nrobi144 ac349665eb fix(desktop): review fixes for workspace management UX
- Remove consumed flag (stale on re-open, double-fire is harmless)
- Fix keyboard nav for WORKSPACES tab (was broken, only worked for screens)
- Reset selectedIndex on tab switch
- Fix delete active workspace → reload columns for new active workspace
- Extract DeckColumnType.param() extension to eliminate 3x duplication
- Simplify moveSelection to handle all modes (tabs + unified search)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 14:55:25 +03:00
nrobi144 09f76f036a feat(desktop): workspace management UX with tabs, editor, unified search
Add two-tab App Drawer (Screens/Workspaces), workspace cards with CRUD,
editor dialog with icon picker and column configuration, unified search
across screens and workspaces, layout mode auto-switching, and
Cmd+Shift+S to save current layout as workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 14:47:24 +03:00
nrobi144 5b8ddf0a20 fix(desktop): review fixes for navigation overhaul
- Fix right-click detection: use isSecondaryPressed (not button.index==2)
- Fix parseColumnType missing drafts/highlights/editor/article cases
- Fix param extraction for Editor.draftSlug and Article.addressTag
- Fix deleteWorkspace index correction logic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 13:47:17 +03:00
nrobi144 e528656ebd feat(desktop): workspace system with save/switch/restore (v1c)
Add workspace presets for different usage modes. Each workspace stores
layout mode + column configuration. Switching destroys current layout
and rebuilds from saved config (no background state).

- Add Workspace data model with WorkspaceColumn
- Add WorkspaceManager with save/load/switch/add/delete
- Add WorkspaceIcons registry for Material icon resolution
- Add WorkspaceBar to AppDrawer footer with workspace chips
- Add DeckState.loadFromWorkspace() for column rebuilding
- Add workspaces persistence to DesktopPreferences
- Default workspace: "Social" (Home + Notifications + DMs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 13:42:30 +03:00
nrobi144 28e9b8202f feat(desktop): customizable nav bar with pin/unpin (v1b)
Add PinnedNavBarState to manage user-customizable sidebar items.
Replace hardcoded navItems in SinglePaneLayout with pinnedScreens
from state, persisted as CSV to DesktopPreferences. Right-click
context menu on App Drawer items to pin/unpin from sidebar.

- Add PinnedNavBarState with pin/unpin/move/save/load
- Add pinnedNavItems to DesktopPreferences (CSV persistence)
- Update SinglePaneLayout to use pinnedScreens from state
- Update AppDrawer with isPinned indicator and context menu
- Remove NavItem data class and hardcoded navItems list
- HomeFeed and Settings are always pinned (non-removable)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 13:37:06 +03:00
nrobi144 e79096f16b feat(desktop): add App Drawer with categorized screen launcher
Replace AddColumnDialog with full-screen App Drawer overlay (Cmd+K).
Screens organized by category (Social, Long-Form, Discovery, Identity,
Play) with instant search, keyboard navigation (arrows + Enter), and
open-column indicators in Deck mode. Works in both Single Pane and
Deck layout modes.

- Add AppDrawer.kt with ScreenCategory, LAUNCHABLE_SCREENS registry
- Add SinglePaneState for hoisted single-pane navigation
- Wire Cmd+K shortcut, redirect Cmd+T to drawer
- Add "More" button to SinglePaneLayout nav rail
- Delete AddColumnDialog.kt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 13:19:39 +03:00
nrobi144 84c4b461a9 fix(release): address code review findings (P1-P3)
P1 fixes (must-fix):
- AppRun: VLC path corrected to usr/lib/app/linux/vlc (jpackage actual
  path), add VLC_PLUGIN_PATH env var for codec discovery
- linuxdeploy: use APPIMAGE_EXTRACT_AND_RUN=1 env var to bypass FUSE
  on CI runners instead of fragile pre-extract + symlink approach
- Prerelease classifier: inverted to positive-match stable format
  (^v[0-9]+\.[0-9]+\.[0-9]+$) across desktop, Android, and composite
  action — eliminates regex drift between allowlists
- assert-stable-release: now accepts inputs (tag, is_prerelease,
  is_draft) so workflow_dispatch on bump workflows also validates

P2 fixes (should-fix):
- .gitignore: add linuxdeploy-*.AppImage and extracted dirs
- RPM version: use replace("-", "~") instead of substringBefore("-")
  for correct RPM prerelease ordering (1.08.0~rc1 < 1.08.0)
- linux-portable → linux alias moved into collect_assets() in
  scripts/asset-name.sh (single source of truth)
- Android cache key: add gradle/libs.versions.toml to hashFiles
- r0adkll/sign-android-release: SHA-pinned to 349ebdef (v1)

P3 fixes (nice-to-have):
- LINUXDEPLOY_OUTPUT_VERSION env var replaced with
  APPIMAGE_EXTRACT_AND_RUN (misleading comment + redundant var)
- nick-fields/retry timeout: 40m → 15m (surface real hangs)
- generate_release_notes: true only on Android job (avoid
  last-writer-wins race across 6 concurrent uploaders)
- apt-get install rpm: tightened guard to matrix.family == 'linux'
  (linux-portable leg doesn't need rpm tooling)
2026-04-17 11:03:01 +03:00
nrobi144 da1037423c feat(desktop): version source-of-truth + RPM + AppImage packaging
Phase 1+2 of multi-platform distribution plan:
- gradle/libs.versions.toml: add `app = "1.08.0"` as single source of truth
- Root build.gradle: allprojects { version = libs.versions.app.get() }
- amethyst/build.gradle: versionName from catalog (versionCode stays local)
- desktopApp/build.gradle.kts: drop hardcoded "1.0.0"; inherit project.version;
  add TargetFormat.Rpm; add linux DSL (menuGroup, appCategory, debMaintainer,
  rpmLicenseType, rpmPackageVersion with dashes stripped)
- desktopApp/build.gradle.kts: new createReleaseAppImage task wrapping
  createReleaseDistributable with linuxdeploy (TargetFormat.AppImage is
  broken in Compose 1.10.x — CMP-7101)
- packaging/appimage/: AppRun launcher (sets LD_LIBRARY_PATH for bundled VLC),
  amethyst.desktop XDG entry, 512x512 icon extracted from icon.icns
- scripts/asset-name.sh: single source for release asset naming contract
2026-04-16 14:35:01 +03:00
Vitor Pamplona e4e7d4816a Fixes desktop compilation 2026-04-15 21:31:40 -04:00
Vitor Pamplona cd8957ad7d Merge pull request #2381 from nrobi144/feat/tor-support
feat: Desktop Tor support — embedded kmp-tor, full privacy routing, settings UI
2026-04-13 11:52:42 -04:00
nrobi144 c9c0cda2c1 feat(tor): full app rebuild on Tor toggle via key() + bootstrap gate + retry
Complete Tor toggle UX:
- key(appRestartKey) wraps App — full Compose tree rebuild on toggle
- TorManager at Window level — survives rebuild, no stop/start collision
- Tor bootstrap gate at top of App — blocks ALL creation (clients, relays,
  Coil, subscriptions) until Tor proxy is ready. Zero clearnet during bootstrap.
- Coil ImageLoader reinitialized after setInstance on each rebuild
- Connection pool evicted on rebuild
- DesktopTorManager retries start up to 3x (handles previous daemon stopping)
- Settings reload from prefs on rebuild (fixes stale toggle display)
- Confirmation dialog on mode change in both TorSettingsSection and Dialog

Known: 2-4 stale Coil CDN connections may persist briefly after toggle
(image loads, not relay traffic — close after OkHttp 5min keep-alive).
Relay WebSocket connections are fully through Tor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:45 +03:00
nrobi144 2df621056e feat(tor): restart confirmation dialog + key() app rebuild on Tor toggle
When user changes Tor settings, shows "Restart Required" confirmation
dialog. On confirm: saves prefs, triggers appRestartKey++ which forces
Compose to unmount/remount entire App tree.

Key behaviors:
- Window stays open, user stays logged in (AccountManager outside App)
- Column layout preserved (DeckState outside App)
- DisposableEffect stops old Tor daemon before new one starts
- TorSettingsSection mode change → confirmation dialog
- TorSettingsDialog Save → confirmation dialog
- Relays, subscriptions, caches all recreated fresh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:45 +03:00
nrobi144 01fc5f3ac6 fix(tor): remove broken runtime reconnect, add evictConnections
Remove the LaunchedEffect/scope.launch reconnect code that didn't
work (relay subscriptions lost after disconnect+connect cycle).
Tor toggle will use app rebuild via key() instead (next commit).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:45 +03:00
nrobi144 70356d541e fix(tor): reconnect relays on Tor OFF too, not just ON
Relay reconnect was only triggered when Tor transitioned to Active.
When toggling OFF, relays stayed on the dead SOCKS proxy and never
reconnected over clearnet — feed went blank.

Now triggers reconnect on both Active and Off transitions, so relays
switch between proxy and direct connections when toggling Tor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:45 +03:00
nrobi144 566a4bf452 fix(tor): wire Coil image loading through Tor via custom Fetcher.Factory
Port Android's OkHttpFactory pattern to desktop — creates a custom
Fetcher.Factory<Uri> that calls DesktopHttpClient.currentClient() per
image request, routing all image loads through the SOCKS proxy when
Tor is active.

Avoids the OkHttpNetworkFetcher.factory() import issue by directly
constructing NetworkFetcher with the Tor-aware Call.Factory, matching
the proven Android implementation.

This closes the last network leak — ALL egress paths now route through
Tor when enabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:45 +03:00
nrobi144 11d0a657ac fix(tor): close 8 network traffic leaks — fail-closed when Tor expected
SECURITY: With Tor ON, all HTTP traffic now routes through SOCKS proxy.
Previously only relay WebSocket connections used Tor; 8 other egress
paths created bare OkHttpClient() instances bypassing Tor entirely.

Fail-closed behavior:
- When Tor expected but bootstrapping → dead SOCKS proxy on port 1
  (requests fail instead of leaking IP)
- lateinit var crashes on misconfiguration (loud failure vs silent leak)

Leak sites fixed (all use DesktopHttpClient.currentClient()):
- AnimatedGifImage: GIF fetch
- SaveMediaAction: media downloads
- EncryptedMediaService: NIP-17 DM media
- ServerHealthCheck: Blossom server probes
- NoteActions: zap/lightning LNURL resolution
- DesktopBlossomClient: media uploads
- AccountManager: NIP-46 bunker relay connections
- Coil image loader: TODO (OkHttpNetworkFetcher import issue)

Also:
- Relay reconnect on Tor Active (prevents stale clearnet connections)
- isTorExpected() for fail-closed logic
- Tests updated for new torTypeProvider parameter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:45 +03:00
nrobi144 8a23a1cd65 fix(tor): move shield to bottom of SinglePane sidebar to prevent cutoff
Shield was getting pushed off screen when RelayHealthIndicator ("x s ago")
appeared above it. Moved to very last position (after BunkerHeartbeat)
so it's always visible at the bottom edge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:44 +03:00
nrobi144 df401c823b fix(tor): add TorStatusIndicator to SinglePaneLayout sidebar
Shield icon was only in DeckSidebar (deck mode) but missing from
SinglePaneLayout's NavigationRail. Added between RelayHealthIndicator
and BunkerHeartbeatIndicator. Clicking navigates to Settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:44 +03:00
nrobi144 4aa4d39a91 fix(tor): default to Internal + Full Privacy, clickable shield, sticky dialog buttons
UX improvements based on manual testing feedback:

- Default TorType changed from OFF to INTERNAL (Tor on by default)
- Default preset changed to Full Privacy (all routing via Tor)
- Shield icon always visible in sidebar (not hidden when Off)
- Shield icon clickable — opens Settings
- TorSettingsDialog Cancel/Save buttons sticky at bottom (don't scroll away)
- HorizontalDivider above buttons for visual separation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:44 +03:00
nrobi144 95ec190b0d feat(tor): wire per-relay routing, .onion badge, shutdown hook, CompositionLocal
Phase 6: Polish and wiring.

Per-relay routing:
- TorRelayEvaluation wired into DesktopHttpClient with actual settings
- shouldUseTorForRelay uses real evaluation instead of { false }
- Settings changes propagate to torTypeFlow/externalPortFlow reactively

CompositionLocal for Tor state:
- LocalTorState provides TorState (status, settings, onSettingsChanged)
- Avoids threading Tor params through every composable layer
- DeckColumnContainer reads from LocalTorState for RelaySettingsScreen

.onion relay badge:
- RelayStatusCard shows "Requires Tor" (red) when Tor is off
- Shows "via Tor" (green) when Tor is active
- Only on .onion relay URLs

Shutdown hook:
- activeTorManager.stopSync() in existing shutdown hook
- Ensures no orphaned Tor processes on app exit
- Set via volatile var from App composable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:44 +03:00
nrobi144 e6068c0916 feat(tor): desktop Tor settings UI — indicator, toggle, dialog
Phase 5: Desktop settings UI for Tor.

TorStatusIndicator:
- Shield icon in sidebar footer (gray/yellow/green/red)
- Tooltip shows status (no port number for security)
- Only visible when Tor is not Off

TorSettingsSection:
- Inline in settings screen between Media Server and Dev Settings
- Mode selector (Off/Internal/External) with segmented buttons
- External SOCKS port input with validation (1-65535)
- "Advanced..." button opens full dialog

TorSettingsDialog:
- Full DialogWindow with preset selector (radio buttons)
- 4 relay routing toggles + 7 content routing toggles
- Preset auto-detection (whichPreset)
- Save/Cancel with DesktopTorPreferences persistence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:44 +03:00