Commit Graph

319 Commits

Author SHA1 Message Date
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
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
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
Claude 6f04edd995 perf(ui): drop remember wrappers where overhead exceeds savings
Cases removed:
- Trivial Modifier allocations (Modifier.weight/padding/size) — slot table
  cost dominates the cost of building a fresh Modifier each recomposition.
- Map.keys views over relayStatuses on desktop screens — .keys is a
  property read on the same map, no need to memoize.
- coerceIn() arithmetic on AudioWaveform Dp/Float params — two compares
  are cheaper than the slot table read+compare.
- fadeIn()/fadeOut() in AnimatedVisibility — small EnterTransition
  allocations that don't justify the slot table overhead.

Audit-only changes; no behavior changes.

https://claude.ai/code/session_011Ea2pVjwvCEx7X4izwryV4
2026-04-26 17:02:53 +00:00
Claude a59358f888 fix(desktop): Messages list typography hierarchy — name vs. preview
Conversation cards had the name (bodyMedium) and last-message preview
(bodySmall) stacked flush against each other with very similar color
weight, so they blurred into one visual block. Now:

- Name: titleSmall (14sp Medium) — the focal top line.
- Preview: bodySmall at onSurfaceVariant.alpha = 0.7 with a 2dp spacer
  above so it sits as a clearly secondary line.
- Timestamp: labelSmall at alpha 0.6, with an 8dp gutter before it so
  long names don't collide with the time.
- Group badge: onSurfaceVariant.alpha = 0.5 instead of primary-70% so
  it doesn't compete with the name for attention.

The name now wins the eye first; the preview reads as a subtitle.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 16:31:01 +00:00
Claude b69224433c Merge remote-tracking branch 'origin/main' into claude/improve-desktop-design-iOokB 2026-04-24 16:20:29 +00:00
Claude beca79d853 fix(desktop): Messages divider blends into both panes
Split the 12dp draggable divider into two 6dp halves: left half takes
surfaceContainer (matching the conversation list pane's fill), right
half takes surface (matching the chat pane's fill). The divider now
reads as the shared edge between the two panes instead of a solid
contrasting stripe cutting down the middle.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 16:17:34 +00:00
davotoula 999184cced optimise imports 2026-04-24 18:15:58 +02:00
Claude aac56bd92e feat(desktop): Messages draggable divider + alignment polish + centered empty states
- **Draggable list pane**: the 280dp-fixed conversation list width is
  now user-adjustable via a draggable divider between the list and the
  chat pane. Width is clamped to 220-480dp and persisted across app
  restarts via DesktopPreferences.messagesListWidthDp. Cursor flips to
  the horizontal-resize arrow on hover.
- **ConversationCard alignment**: the unread indicator used to sit as
  an 8dp dot + 14dp spacer to the left of the avatar, which pushed
  every card ~14dp inward from the header row's 12dp padding. The
  avatar now starts flush with the header (aligned with the top-bar
  buttons); the unread state is drawn as a 10dp dot overlaid on the
  avatar's bottom-right corner (iMessage / Slack-style), with a
  surface-matched ring so it reads clearly against the avatar.
- **Drafts empty state**: was a flush-left Text pinned 16dp below the
  header; now uses the shared EmptyState composable so "No drafts
  yet" / description is vertically + horizontally centered in the
  available space, consistent with Notifications / Highlights / etc.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 16:14:18 +00:00
Claude c5712af2de refactor(desktop): move commons/ui/chat into desktopApp
Every file under commons/src/commonMain/kotlin/.../commons/ui/chat/
(ChatBubbleLayout, ChatMessageCompose, ChatroomHeader,
DmBroadcastBanner, DmBroadcastStatus, UserDisplayNameLayout) was
imported only by desktopApp. Android has its own parallel
implementation at amethyst/.../chats/feed/, so nothing shared.

Moved the six files into desktopApp/.../ui/chats/ alongside
ChatPane.kt, DesktopMessagesScreen.kt, etc. — same package as every
existing caller, which lets us drop six `import` lines from
ChatPane.kt + DmSendTracker.kt + DesktopMessagesScreen.kt. Empty
commons/ui/chat/ directory removed.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 16:09:25 +00:00
Claude fc9eb833ac fix(desktop): remove ID footer/divider, whole-card hover on NoteCard
- Dropped the "ID: <hex>…" debug footer and the HorizontalDivider that
  sat above it. Cards read as finished content now, not dev output.
- The previous design had an inner header+text Column carrying the
  clickable modifier, which meant the hover ring only covered part of
  the card (a rectangle inside). Switched to Material3 Card(onClick =
  onClick, …) so the whole card is the click-surface and the ripple is
  clipped to the card's rounded shape by M3 itself.
  - Content moved into a `cardBody` lambda reused by both branches of
    the `if (onClick != null)` check — non-clickable fallback exists
    for in-thread quoted note wrappers.
  - Action buttons inside NoteActionsRow have their own clickables that
    consume taps before they reach the Card's handler, so tapping
    reply / repost / zap still fires only that action, not the Card.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 16:06:08 +00:00
Claude bbe84c2bb0 fix(desktop): BasicTextField+DecorationBox for compact Search/Chat inputs
M3 OutlinedTextField has ~32dp of vertical contentPadding baked in,
so forcing .height(40-44dp) clipped the bodyMedium (14sp, 20dp line-
height) placeholder vertically.

Refactored both Search and Chat inputs to BasicTextField wrapped in
OutlinedTextFieldDefaults.DecorationBox, which lets us override
contentPadding to (12.dp horizontal, 8.dp vertical). The field now
renders at a true 40dp tall with the placeholder centered and no
clipping. Border, focus states, placeholder, and leading/trailing
icons still come from M3 so the look stays consistent.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 16:02:00 +00:00
Claude 8ae4ce6f91 fix(desktop): compact Profile header buttons
"Edit Profile" was an OutlinedButton with icon + text which rendered
at ~40dp tall — taller than the 32dp IconButtons every other screen
uses in its header row, so it was pushing the Profile title down and
inflating the top border of the whole page.

- Edit Profile → IconButton(size = 32.dp) with Edit icon (size = 20.dp)
  tinted primary. Matches the + / refresh / relays pattern on Home,
  Reads, Notifications, etc.
- Follow / Unfollow button (when viewing someone else's profile) —
  kept as a labelled Button for clarity but compacted to height 32.dp
  with contentPadding(horizontal = 12.dp, vertical = 0.dp) so it
  matches the header row's scale.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 15:57:49 +00:00
Claude 7da69fbaee fix(desktop): hover-state polish + Search width cap + reserve reaction-icon space
NoteCard hover (header+text inner clickable + avatar/name chip):
- Clip(RoundedCornerShape(8.dp)) before .clickable on the header+text
  Column so the ripple rounds instead of cutting a hard rectangle.
- Avatar+name chip gets a stadium clip (RoundedCornerShape(100.dp))
  before its clickable — matches how Slack / Notion / Linear render
  user-pill hover states.

Chat bubble hover (ChatBubbleLayout):
- combinedClickable was applied outside the Surface's shape clipping,
  so the ripple ignored ChatBubbleShapeMe/Them. Moved the shape into a
  named val and added Modifier.clip(bubbleShape) ahead of
  combinedClickable. Hover now rounds with the bubble.

Chat reaction icon (ChatPane detailRow):
- On hover the "add reaction" icon used to conditionally appear and
  shift every other element in the detail row, causing the whole list
  to reflow up or down. Wrapped in a Box with Modifier.alpha that
  fades in/out — the icon is always laid out so the row stays fixed.
  Button is disabled when not hovered so it's click-through when
  invisible.

Search text field:
- Now padded by LocalReadingSidePadding so it stays inside the 720dp
  reading column on wide displays (previously the search Row's
  fillMaxWidth spanned the whole window after the ReadingColumn
  refactor).
- Bumped height from 40dp → 44dp. M3 OutlinedTextField has ~16dp of
  internal vertical content padding that was clipping the bodyMedium
  placeholder at 40dp. Same change for the chat input.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 15:55:57 +00:00
Claude 3b9ef980a6 fix(desktop): full-width scroll with centered content via LocalReadingSidePadding
The widthIn cap confined every scrollable element to a 720dp column in
the middle of the window. Anywhere the mouse hovered outside that
column, the scroll wheel did nothing — scroll events only reached the
LazyColumn when the cursor was inside.

Refactored ReadingColumn to use BoxWithConstraints + a CompositionLocal
(LocalReadingSidePadding) instead of a width-capping Box wrapper. The
outer Column now fills the whole window so scroll gestures land on the
scrollable wherever the mouse is. Each screen reads the side padding
from the CompositionLocal and applies it to:
- its header Row as `horizontal = readingHorizontalPadding()`
- its LazyColumn as `contentPadding = PaddingValues(horizontal = readingHorizontalPadding())`

Items still appear centered at DefaultReadingWidth (720dp), but the
scrollable surface now spans the full window width.

Also:
- Search placeholder shortened ("Search people, tags, notes…") so it
  stops getting cut off in the compact 40dp field.
- UserProfile's floating header AnimatedVisibility call needed to be
  fully-qualified (androidx.compose.animation.AnimatedVisibility) to
  avoid the compiler picking the ColumnScope overload inherited from
  ReadingColumn's outer ColumnScope — the inner BoxScope's
  Modifier.align(Alignment.TopCenter) required the non-scoped variant.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 15:49:24 +00:00
Claude 6c33279449 fix(desktop): compact text fields + normalize Settings section headers
Text fields:
- Chat input (ChatPane) and Search field (SearchScreen) both used
  M3 OutlinedTextField defaults, which has a 56dp min-height tuned
  for mobile touch. On desktop that reads as "way too tall" — roughly
  double what Slack / Raycast / VS Code show. Overrode with
  Modifier.height(40.dp) / heightIn(min = 40.dp), paired with
  bodyMedium (14sp) textStyle and slightly smaller leading/trailing
  icons so the field sits at a desktop-native ~40dp when empty, still
  grows with content in the chat case.

Settings section headers — normalized to titleSmall (14sp) so every
section sits consistently under the "Settings" titleMedium (16sp)
screen title:
- "Wallet Connect (NWC)" (was titleLarge, already fixed)
- "Relay Settings" (was titleLarge, already fixed)
- "Tor" (was titleLarge, now titleSmall)
- "Media Servers (Blossom)" (was titleMedium, now titleSmall)

MediaServerSettings extra border removed — the parent
RelaySettingsScreen already provides a 12dp horizontal gutter, but
MediaServerSettings was adding another 16dp all-sides padding on top,
showing as a visible inner frame. Dropped that wrapper padding.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 15:33:38 +00:00
Claude c4ae0a30eb fix(desktop): width cap ordering, platform icon weight, Settings hierarchy
Three related fixes:

ReadingColumn modifier ordering:
- fillMaxSize().widthIn(max = 720) was locking width to parent before
  widthIn could cap it, so the cap had no effect in practice. Switched
  to widthIn(max = 720).fillMaxWidth().fillMaxHeight() which now caps
  correctly on wide displays. Same fix applied to the three screens
  that use Box+widthIn inline (UserProfile, Search, Settings).

Platform-aware Material Symbols weight:
- ProvideMaterialSymbols now takes an optional weight parameter that
  defaults to MaterialSymbolsDefaults.WEIGHT (300) so Android keeps
  current behavior. Desktop passes PlatformIconWeight.current which
  maps:
    macOS   → 200 (thin, matches SF Symbols stroke)
    GNOME   → 300 (matches libadwaita symbolic icons)
    KDE     → 300 (matches Breeze)
    Windows → 400 (matches Fluent Icons)
    other   → 300

Settings section hierarchy:
- "Wallet Connect (NWC)" and "Relay Settings" section headers dropped
  from titleLarge (22sp) to titleSmall (14sp) so they're smaller than
  the "Settings" screen title (titleMedium 16sp). Restores the
  visual hierarchy the user reported inverted.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 15:27:53 +00:00
Claude cd4dd42bc4 fix(desktop): clip card ripples to card shape via Card(onClick = ...)
Material3 Card has a built-in onClick parameter that handles the
ripple with proper rounded-corner clipping. Using Modifier.clickable
on the Card's modifier bypasses that — the ripple renders as a
rectangle and gets awkwardly cut off at the card corners.

Converted to the onClick-parameter form on:
- LongFormCard (Reads)
- DraftCard (Drafts)
- RelayMetricCard (Relay Dashboard / Monitor tab)

NoteCard intentionally left unchanged: its inner clickable covers
only the header+text region (not the action-buttons row), so a
full-card onClick would conflict with the per-action handlers.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 15:14:08 +00:00
Claude d212b88103 feat(desktop): ReadingColumn width cap, conditional Profile back, card styling pass
Introduces ReadingColumn, a top-level scaffold that caps feed/list
screens at 720 dp and centers them — matches the Twitter/Mastodon
desktop pattern so cards don't stretch disproportionately on 4K
displays. Applied to: Home, Reads, Notifications, Bookmarks, Drafts,
Highlights, Search, Thread, Profile, Settings. Messages stays
full-width (its own two-pane sizing). Chess, Relay Dashboard, Article
Reader/Editor keep their current full-width layouts (tools/reading
logic dictates width independently).

Header consistency:
- Bookmarks / Drafts / Highlights / Search now have a minimum header
  row height of 48dp so screens without action buttons sit at the
  same visual weight as screens with IconButtons.
- Drafts' "New Draft" button converted from text Button to IconButton
  for consistency with the other screens' icon-only actions.
- Settings title switched from headlineMedium to titleMedium and
  wrapped in the standard h=12/v=8 header row.

Profile back button:
- UserProfileScreen now takes canGoBack: Boolean = false. The back
  arrow only renders when stacked onto a nav stack (clicking a user
  in the feed / notifications). Top-level "My Profile" accessed via
  the nav rail has no back arrow — nothing above it to pop.
- Applied to both the in-header back button and the floating
  scroll-aware header that appears when scrolling through posts.

Home cards:
- NoteCard switched from surfaceVariant fill (gray) to surface (white)
  + 1dp elevation, matching LongFormCard on Reads. The subtle shadow
  gives the feed the same lift as the articles screen.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 15:11:48 +00:00
Claude 08f9c8bfd4 feat(desktop): 12dp horizontal gutter around feed/list content
Content cards were butting up against the nav rail / window edge.
Added contentPadding = PaddingValues(horizontal = 12.dp) to every
scroll container so items sit inset by the same amount the header
row already uses — a consistent gutter the eye can anchor to.

Applied to: FeedScreen, ReadsScreen, NotificationsScreen,
BookmarksScreen, DraftsScreen, SearchScreen, MyHighlightsScreen,
UserProfileScreen, ThreadScreen, ChessScreen, RelayMetricsTab.
RelayConfigTab (verticalScroll Column) got the same horizontal
padding via its content modifier.

Messages is untouched — its two-pane layout with an internal
VerticalDivider already provides its own visual separation.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 14:55:20 +00:00
Claude ff12b6abbe feat(desktop): Messages-style headers across every remaining screen
Rolled the compact header pattern out to the screens the earlier pass
missed. Each one now lives on the same padding/typography rhythm as
Messages: Row(fillMaxWidth, padding horizontal = 12, vertical = 8),
titleMedium for labels, IconButton size = 32dp with 20dp icons tinted
with colorScheme.primary.

- ThreadScreen: back button + "Thread" title. Was headlineMedium with
  bottom-only padding; now compact row.
- UserProfileScreen: back + "Profile" title on the left, Edit /
  Follow buttons kept on the right (they stay as text buttons — they
  represent destructive intent, not icon affordances).
- ArticleReaderScreen: back + "Article" title; zoom % label still
  surfaces next to the title when != 100%.
- ArticleEditorScreen: back IconButton + "Article" title (was a text
  "Back" OutlinedButton with no title). Save / Publish buttons kept
  on the right — same reasoning as UserProfileScreen.
- ChessScreen: back (conditional) + "Chess" / "Live Game" title;
  refresh + New Game converted from Button → IconButton so the
  header reads at the same scale as the rest.
- RelayDashboardScreen: had no header at all, just a PrimaryTabRow.
  Converted Monitor / Configure to FilterChip tabs-first (matching
  Feed / Reads) so the selected tab is the title.

FeedHeader relocation:
- Moved commons/ui/feed/FeedHeader.kt → desktopApp/ui/FeedHeader.kt.
  Only NotificationsScreen.kt referenced it, and dropping the import
  from that file was enough because it's now in the same package.
- Removed the empty commons/ui/feed/ directory.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 14:38:43 +00:00
Claude b198385849 feat(desktop): tabs-first header for Home / Reads, compact Notifications header
Refactored the three feed screen headers so they all match the Messages
pattern — single compact row padded horizontal = 12, vertical = 8, no
oversized titles, no multi-row metadata.

FeedScreen (Home):
- Replaced the `headlineMedium` "Global Feed" / "Following Feed" title
  plus a redundant Global/Following chip row plus a separate relay-count
  meta-line plus a large "New Post" button with:
    [ Following | Global ]   [relays] [refresh] [+ compose]
  The selected tab IS the screen title. Relay count + followed-user
  count surface through a hover tooltip on the relays icon (desktop
  convention; info is preserved without taking a whole row).
- Relays icon click opens the picker when the account can edit, falls
  through to navigate-to-relays otherwise.

ReadsScreen:
- Same treatment: dropped the "Reads" label, lifted Following/Global
  chips to the left, single refresh icon on the right. "N relays
  connected" moved to the refresh button's content description.

commons/FeedHeader (used by NotificationsScreen):
- Dropped the RelayStatusIndicator (icon + count text + refresh button,
  took 3 slots). Now just titleMedium on the left + a single refresh
  IconButton on the right with relay count in its content description,
  matching the Messages "titleMedium + icon buttons" rhythm.
- Internal padding(horizontal = 12, vertical = 8) baked in so callers
  don't need a trailing Spacer.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 14:23:32 +00:00
Claude 8fb16bc7a3 fix(desktop): bump macOS icon mark to 90% of squircle
Mark padding inside the squircle down from 7.5% to 5% each side, so the
goose fills the squircle a bit more fully and matches the visual weight
of neighboring dock icons.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 14:11:36 +00:00
Claude f3ef3514b2 feat(desktop): icon mark sized to match neighbors, add drop shadow, unify screen headers
Icon:
- Mark now fills ~85% of the squircle (was 80%) so the goose reads at
  the same visual weight as first-party dock icons. Apple's template
  specifies 7.5-10% padding inside the squircle; 7.5% lands closer to
  the average first-party icon.
- Baked a drop shadow into the icon PNG: 8px offset, 18px Gaussian
  blur at ~28% black, rendered under the white squircle on its own
  layer so the blur doesn't leak into the mark. First-party macOS
  icons include this shadow in the PNG — the dock doesn't add one at
  render time. Separable Gaussian (2x 1D passes) keeps startup fast.

Screen header consistency (match Messages pattern):
- Bookmarks, Drafts, Search, Reads, Highlights: titles switched from
  headlineMedium to titleMedium.
- All five header rows now pad horizontal = 12.dp, vertical = 8.dp
  (matching ConversationListPane's "Messages" header).
- Removed the 16.dp outer wrapper padding from Bookmarks and the 16.dp
  bottom padding from Reads header — screens now sit edge-to-edge.
- DeckColumnContainer's 12.dp outer padding around column content
  removed for the same reason SinglePaneLayout's was: creates a
  `#F5F5F7` frame around every screen that reads as inconsistent
  with Messages (and every native desktop app).

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 14:07:39 +00:00
Claude be8fef15db fix(desktop): match Apple HIG squircle margins so dock icon isn't oversized
Previous impl filled the entire 1024x1024 canvas with the squircle.
Apple's macOS app icon template leaves ~100px of transparent margin
around a centered 824x824 squircle (content area is 80.5% of the
canvas width) — which is the size the dock normalizes to when laying
out next to first-party apps. Filling the whole canvas made Amethyst
render ~20% larger than its neighbors in the dock and Cmd+Tab.

Now matches Apple's reference geometry:
  - Canvas: 1024x1024 (transparent margins of 100px)
  - Squircle: 824x824 centered
  - Corner radius: ~185px (22.45% of the squircle, per HIG)
  - Mark: 10% padding inside the squircle

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 13:59:29 +00:00
Claude 979c262d87 fix(desktop): drop 12dp border around single-pane content
SinglePaneLayout wrapped the main content area in `.padding(12.dp)`,
leaving a visible strip of the window background around every screen.
On macOS with the theming updates this strip became very noticeable:
conversation list, chat content, and the NavigationRail no longer
shared a single edge, and you could see `#F5F5F7` framing the whole
content block.

Native desktop apps don't frame content like that — padding is added
inside cards / lists / dialogs, not around the window content area.
Removing the wrapper padding lets the Messages two-column layout
(surfaceContainer sidebar + surface chat) run edge-to-edge.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
2026-04-24 13:58:10 +00:00