Commit Graph

12501 Commits

Author SHA1 Message Date
Claude b0f75d4dd4 fix(home-banner): float DVM status banner over the feed instead of in the top bar
Putting HomeDvmStatusBanner inside the top-bar Column meant the
SecondaryTabRow shifted up/down every time the banner appeared or
disappeared (filter selection, refresh, response arrival). Annoying.

Move the banner into a Box that wraps the HorizontalPager and align
it to TopCenter so it floats over the feed:
- topBar Column is back to just HomeTopBar + SecondaryTabRow; tabs
  no longer reflow when the banner toggles.
- BannerCard takes a Modifier and gets tonalElevation + shadowElevation
  + a slightly stronger surface tint so it reads as a floating overlay
  rather than part of the scaffold chrome.
2026-04-19 15:53:23 +00:00
Claude e244271bd0 feat(pdf): zoom-aware hi-res re-render for crisp pinch-zoom
Base bitmap stays at 3072 px for the initial render. When the user
zooms past 1.2x and scale settles for 200 ms, asynchronously re-render
the current page at (VIEWER_MAX_DIM_PX * scale) capped at 6144 px
(~100 MB peak for an A4 page). Swap the hi-res bitmap in while zoomed;
revert to base when scale drops back under threshold or the page
leaves composition. Only the currently-focused page gets the hi-res
treatment.

Also apply FilterQuality.High to the Image composable in both the
viewer and the preview card so any residual GPU upscaling uses
bicubic-ish sampling instead of bilinear.

Factored the render path into a shared renderPageCatching() helper.
2026-04-19 15:46:25 +00:00
Claude 93cf9f1d6e refactor(badges): drop OutlinedCard chrome from feed items
Each badge was wrapped in its own rounded OutlinedCard, which reads as
an out-of-place boxed widget in the feed since every other feed item
is a flat row separated by the standard HorizontalDivider drawn by
FeedLoaded.

Replace the Card with a plain Column + clickable + padding. The
feed's own divider handles item separation and the UI now matches
Notes, Articles, Pictures, etc.
2026-04-19 15:40:30 +00:00
Claude 3730d2fb73 fix(ui): keep chrome visible on chat detail screens
Chat detail screens (DMs, public chat, live-activity chat, ephemeral
chat, marmot group) have a compound layout that doesn't play well with
the scroll-under-bars model:

  Column {
      (optional) relay-warning header
      weight(1) Column { reverseLayout LazyColumn }
      Spacer
      PrivateMessageEditFieldRow   // stays above the keyboard, has
                                   // its own IME / nav-inset handling
  }

Making the messages scroll behind the top bar while the input field
stays planted above the keyboard (and the warning header sits below
the bar when present) requires a Box overlay + measured-input-height
trick that's incompatible with the existing Column + weight layout.
Most chat apps (Telegram, Signal, WhatsApp, etc.) keep chrome pinned
on a conversation screen anyway, so the simple and correct answer is
to just not hide the chrome here.

- New `allowBarHide: Boolean = true` parameter on `DisappearingScaffold`.
  When false, `canScroll` returns false and the NSC no-ops, which keeps
  the top bar pinned regardless of `isImmersiveScrollingActive()`.
- All 6 chat detail screens pass `allowBarHide = false`:
  - ChatroomScreen
  - ChatroomByAuthorScreen
  - PublicChatChannelScreen
  - EphemeralChatScreen
  - LiveActivityChannelScreen
  - MarmotGroupChatScreen

The existing `Column(Modifier.padding(it)...)` layout is correct when
the bars are pinned (no strip because the bar never translates), so no
other changes needed inside the chat views themselves.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
2026-04-19 15:37:21 +00:00
Claude dc41432c5b fix(topnav): long filter names no longer wrap + hide expand icon
Selecting a DVM with a long name (or any long-named filter) made
the Text in the top-nav spinner wrap to two lines, pushing the
ExpandMore icon out of the visible area. The Column holding the
Text had no width constraint and the Text itself had no maxLines.

- Constrain the Column with `Modifier.weight(1f, fill = false)` so
  it reserves space for the icon instead of consuming it.
- Add `maxLines = 1, overflow = TextOverflow.Ellipsis` to every
  text path in the spinner (primary label, geohash city-loading,
  AroundMe location labels). Single-line + ellipsis for all filter
  types, icon always visible.
2026-04-19 15:37:07 +00:00
Claude 013c211e6a fix(pdf): top-align viewer controls and bump render resolution
- Wrap pager + controls Row in a Box(fillMaxSize) and align the Row to
  TopCenter so the back/share buttons sit at the top of the screen,
  matching ZoomableContentDialog's layout. They were previously
  vertically centered.
- Raise VIEWER_MAX_DIM_PX from 2048 to 3072 so pinch-zoomed pages stay
  legible. A4-sized pages now render at ~26 MB each (ARGB_8888).
- Replace the unbounded mutableStateMapOf page cache with a tiny
  LinkedHashMap-based LRU (PAGE_CACHE_SIZE = 3) to keep total bitmap
  memory around 80 MB regardless of PDF length. The cache only feeds
  produceState's initial value, so it doesn't need to be a snapshot
  state.
2026-04-19 15:21:01 +00:00
Claude bc8edf9523 feat(badges/profile): live updates and dedicated relay subscription
ProfileBadgesScreen used to compute the received-awards list once via a
plain remember and never refresh it; new awards landing in LocalCache
were invisible until the user navigated away and back. There was also
no relay subscription dedicated to back-filling award history — we
relied on the always-on notifications subscription bounded by `since`,
so older awards never arrived.

- New ProfileBadgesFilterAssembler / SubAssembler / Subscription that,
  while the screen is mounted, queries kind 8 with `#p`=me on the
  user's notification relays (limit 500, no since) so the full history
  flows in.
- Registered as `profileBadges` in RelaySubscriptionsCoordinator.
- ProfileBadgesScreen now collects LocalCache.live.newEventBundles in
  a LaunchedEffect, bumping a tick whenever a bundle contains a
  BadgeAwardEvent for me. The receivedAwards remember keys on that
  tick, so newly arrived awards appear without leaving the screen.
- AwardRow now resolves the badge definition via LoadAddressableNote +
  observeNoteEvent + EventFinderFilterAssemblerSubscription so each
  row re-renders when the linked kind 30009 lands in cache (and asks
  relays for it if missing). The Switch is disabled until the
  definition is available.
2026-04-19 15:20:22 +00:00
Claude 64079d4188 feat(badges/award): swap raw pubkey textarea for user search
The award screen now collects awardees the same way other selection
screens do (AddMemberScreen pattern):
- A search OutlinedTextField wired into UserSuggestionState +
  ShowUserSuggestionList. Typing >2 chars triggers the existing user
  search pipeline.
- Selecting a suggestion adds the user to a header list of selected
  recipients (avatar, display name, NIP-05 / pubkey), each with a
  Remove button.
- Submit button disables until at least one recipient is picked.

ViewModel reduced to definition + sendPost(awardees: List<User>);
parsedPubKeys / awardeesText state removed.
2026-04-19 15:11:58 +00:00
Claude c2101c16dc fix(pdf): avoid closing renderer via delegated read in DisposableEffect
DisposableEffect(handleState) captured handleState as a property
delegate, so onDispose read the *current* delegated value at dispose
time. When the async load transitioned handleState from null to the
new handle, the previous DisposableEffect(null) was forgotten and its
onDispose fired — reading the freshly-created handle via the delegate
and closing it immediately. That left the dialog stuck on the loading
spinner (page renders bailed out due to the closed flag) and double-
closed the renderer on dismiss.

Capture the handle as a local val before DisposableEffect so the
lambda closes the specific handle that was current at effect creation.
2026-04-19 15:02:46 +00:00
Claude 9b409723bd fix(ui): thread scaffold padding through Messages, DVMs, Search
Audit follow-up to the previous padding migration: four more
`DisappearingScaffold` consumers were still using the old
`HorizontalPager(contentPadding = it)` or `Modifier.padding(it)`
patterns, so their feeds didn't extend behind the chrome.

- Messages single-pane: `MessagesPager` no longer applies the scaffold
  padding to the pager's `contentPadding` (which shrinks pages); it
  threads it down to `ChatroomListFeedView` instead, which now accepts
  `scaffoldPadding` and applies it on its inner `LazyColumn` via
  `rememberMergedPadding(scaffoldPadding, FeedPadding)`.
- Messages two-pane: drops the outer `Modifier.padding(padding)
  .consumeWindowInsets(padding)` on the `TwoPane` and threads the
  padding through `ChatroomList` → `MessagesPager` →
  `ChatroomListFeedView`.
- `DvmContentDiscoveryScreen`: drops the wrapping `Column(Modifier
  .padding(paddingValues))` and threads the scaffold padding through
  `DvmContentDiscoveryScreen` → `ObserverContentDiscoveryResponse` →
  `PrepareViewContentDiscoveryModels` → `RenderNostrNIP90Content
  DiscoveryScreen` → `RenderFeedState`'s default `FeedLoaded`.
- `SearchScreen`: drops the wrapping
  `Column(Modifier.padding(it).consumeWindowInsets(it))` and turns the
  inbox-relay warning card into a `headerContent` slot rendered as the
  first item of the search-results `LazyColumn`. The list now also
  always exists (with the early `return` moved into the lazy-list
  builder via `return@LazyColumn`) so the header is visible even when
  no search is in progress.

Settings (non-scrollable) and chat detail screens (inverted layout
plus a custom input field at the bottom — these need a more careful
refactor to handle IME + nav inset together) intentionally left
unchanged.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
2026-04-19 14:59:57 +00: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
Vitor Pamplona 5240f29634 Merge pull request #2447 from vitorpamplona/claude/content-warning-media-overlay-zjdoP
Refactor content warning UI with backdrop support and image preloading
2026-04-19 10:56:56 -04:00
Claude 876513ad58 fix(pdf): capture page count eagerly to survive PagerState teardown
PagerState's saveable reads the pageCount lambda during composition
teardown, which runs *after* DisposableEffect.onDispose has already
closed the PdfRenderer. That triggered
IllegalStateException("Document already closed") the first time the
dialog was dismissed.

Store pageCount as a stored val sampled at handle construction instead
of re-querying the renderer. Also add a @Volatile closed flag so the
PdfPageView render coroutine bails out if it wakes up after close();
existing catch clause still swallows any narrow race.
2026-04-19 14:55:32 +00:00
Claude dcaa6607ea perf(feeds): remember isSensitive and content warning reasons per note
These derivations iterate every tag and parse every iMeta, so running
them on each recomposition churns through feed scrolls. Cache them in
remember(note) alongside the other per-note derivations.
2026-04-19 14:47:20 +00:00
Claude 55679b0a9e fix(badges): apply scaffold padding and make cards clickable
BadgesScreen dropped the DisappearingScaffold's paddingValues on the
floor, so the first list item hid behind the top bar and the last
behind the bottom bar. Wrap the feed in Column(Modifier.padding(...))
like ArticlesScreen.

BadgeCard was a bare OutlinedCard with no click target, so tapping a
badge definition or award card did nothing. Thread a nullable onClick
through BadgeCard; BadgeDisplay routes to the definition's thread and
RenderBadgeAward routes to the award's thread.
2026-04-19 14:38:14 +00:00
Claude d8b313088d fix(dvm-favorites): star click is a no-op + star missing from DVM detail top bar
Two distinct bugs with the same symptom ("clicking the star doesn't
seem to do anything"):

1. LocalCache didn't know how to route kind 10090. FavoriteDvmListEvent
   was missing from LocalCache's event-type dispatch, so after
   Account.followFavoriteDvm signs and publishes the event, the
   cache silently dropped it — never updated favoriteDvmListNote,
   so account.favoriteDvmList.flow never re-emitted, so the star's
   filled/outlined state (and the spinner chip) never flipped. Add
   `is FavoriteDvmListEvent -> consumeBaseReplaceable(...)`.

2. DvmTopBar's toggle never rendered. The Home feed passes the DVM's
   hex event id via Route.ContentDiscovery, so LoadNote(baseNoteHex)
   returns a plain Note, not the AddressableNote the toggle needs
   (the `is AddressableNote` guard was always false). Derive the
   AddressableNote from the loaded AppDefinitionEvent.address() so
   the toggle shows up with the correct backing object.
2026-04-19 14:34:07 +00: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
Claude 95b49879f4 refactor(profile): refine badge strip on profile header
- Drop the bare octagonal FlowRow of 35dp thumbs. Replace with a
  labeled strip ("Badges · N") of 44dp rounded-square thumbs matching
  the BadgeCard language used elsewhere.
- Cap the visible row at 8 badges and surface overflow as a "+N" pill
  that opens a ModalBottomSheet listing every accepted badge with its
  thumbnail, name, and description. Tapping a row closes the sheet and
  navigates to that badge's thread.
- When viewing your own profile, add a settings gear trailing the
  header that jumps to Route.ProfileBadges to manage which badges
  appear.
- Skip the entire strip (no empty header, no padding) until at least
  one badge is present.
2026-04-19 00:47:35 +00:00
Claude 9ba2d2f5cc feat(dvm-favorites): timeout, tests, and merged "All favourite DVMs" chip
1. DVM response timeout. FavoriteDvmOrchestrator now times out after
   20s if neither a 6300 response nor any 7000 status arrives, and
   sets errorMessage = "timeout" on the snapshot so the home banner
   switches from the "Asking…" spinner to a Retry button instead of
   hanging forever.

2. Tests. FavoriteDvmListEventTest covers create/add/remove round
   trips and the fixed-empty d-tag invariant; FavoriteDvmTopNavFilter
   match by id and by `a` address; FilterHomePostsByDvmIdsTest covers
   the two-relay-set split (content fetch on user relays, listen on
   DVM relays) and the multi-requestId merge case.

   Also registered kind 10090 in EventFactory so Quartz can deserialise
   FavoriteDvmListEvent (required for the round-trip tests and for
   reading the list back from relays).

3. Merged "All favourite DVMs" chip. New TopFilter.AllFavoriteDvms
   that unions every favourite's latest 6300 response into one feed.
   AllFavoriteDvmsFeedFlow uses flatMapLatest over the favourite-list
   flow so subscriptions rewire when the user adds/removes a DVM.
   FavoriteDvmTopNavPerRelayFilterSet now carries Set<HexKey>
   requestIds (was a single nullable) so the filter can subscribe to
   N kind 6300/7000 streams in one REQ per DVM relay. Banner renders
   "Asking your favourite DVMs for feeds…" while all are pending and
   a single Retry-all on collective error; pull-to-refresh re-issues
   every DVM's kind-5300.
2026-04-19 00:12:26 +00:00
Claude 9fbe8eba78 refactor(pdf): share Coil disk cache, gate on showImages, cap bitmap size
- PdfFetcher now reuses Amethyst.instance.diskCache (Coil) via
  openSnapshot/openEditor instead of a custom cacheDir folder. PDFs share
  the same LRU budget as images and benefit from automatic eviction.
- PdfPreviewCard now respects accountViewModel.settings.showImages():
  when disabled, shows a lightweight "Tap to load PDF" placeholder and
  only downloads+renders after the user opts in.
- Both the card thumbnail (1600px) and viewer page (2048px) bitmaps are
  capped to a maximum longest-side dimension, preventing OOM on very
  large or unusually tall PDF pages.
- PdfViewerDialog holds the cache snapshot for the dialog's lifetime so
  the underlying file can't be evicted mid-view, and closes it in
  DisposableEffect alongside the renderer and ParcelFileDescriptor.
2026-04-18 23:59:50 +00:00
Claude 60a56440d1 refactor(feeds): unify content warning behind ContentWarningGate
Collapse the per-media, per-blurhash and grid-of-blurhash variants into a
single ContentWarningGate that takes a backdrop slot and a sizing
modifier. Callers choose the backdrop (single blurhash, grid of
blurhashes, or none).

Remove the isSensitive field from MediaUrl{Image,Video}; it was a
rendering concern leaked into a media model. Sensitivity is now only
expressed to the gate. Picture/Video feeds and their display variants
compute isSensitive + reasons themselves and wrap with the gate.

ZoomableContentView keeps a default internal gate (fires when
content.contentWarning != null) so the 14 callers that don't need
grid-level handling still get the blurhash overlay automatically.

Also rename collectPictureReasons to collectContentWarningReasons.
2026-04-18 23:58:36 +00:00
Claude 7a8dc02394 refactor(badges): single feed + top-nav filter, profile badges to settings
Restructure the Badges screen to match Polls and other feeds:
- Drop the 4-tab pager in favor of a single feed of BadgeDefinitionEvent
  (kind 30009), with a FeedFilterSpinner in the top bar.
- Introduce TopFilter.Mine as a selectable option so the same spinner
  switches between follow-list semantics and "only badges I authored".
  Defaults to AllFollows.
- New unified BadgesFeedFilter reading defaultBadgesFollowList.
- BadgesSubAssembler now uses PerUserAndFollowListEoseManager (limit
  100). Mine subscribes to outbox with authors=me; everything else
  dispatches makeBadgesFilter across follow-list/global/authors/muted
  per-relay filter sets, identical in shape to the Polls pipeline.
- Feed states: replace badgesReceived / badgesMine / badgesAwarded /
  badgesDiscover with a single badgesFeed.

Navigation into a badge definition now surfaces its full award history:
BadgeAwardEvent.KIND is added to RepliesAndReactionsToAddressesKinds1,
so the existing thread view of a kind 30009 note pulls in every kind 8
referencing it via the `a` tag.

Received-badge management moves to a dedicated settings page:
- New Route.ProfileBadges + ProfileBadgesScreen listing every
  BadgeAwardEvent where I'm a `p` recipient with a Switch per row
  that toggles it into the ProfileBadgesEvent (10008).
- Linked from AllSettingsScreen via a MilitaryTech row.
2026-04-18 23:45:26 +00:00
Claude ecdbc80fc1 feat: preview PDF links inline with first-page thumbnail and full pager
Adds MediaUrlPdf / PdfSegment to the rich-text pipeline so PDF URLs
(detected by .pdf extension, NIP-92 imeta m tag, or application/pdf
Content-Type) render a card showing the first page, filename, and page
count. Tapping opens a full-screen HorizontalPager over every page
rendered on demand with PdfRenderer. Long-press surfaces the existing
share menu. Uses only the built-in Android PdfRenderer; no new
dependencies. Desktop continues to fall back to a clickable link.
2026-04-18 23:06:49 +00:00
Claude 309e474a3d fix(dvm-card): star top-right + move reactions to bottom row
Putting the star back where the user actually sees DVMs first — the
Discover Content card. Two changes that together stop the layout
from breaking:

- FavoriteDvmToggle now uses ClickableBox + small Icon (no IconButton
  padding) so it occupies the same compact footprint as LikeReaction.
  Adding it to LeftPictureLayout's title row no longer inflates the
  row height.
- DVMCard's title row now holds only the name (weight 1) plus the
  star toggle on the right. LikeReaction and ZapReaction move down
  to the bottom row, pushed to the far right with a Spacer(weight 1)
  so the amount/personalised chips stay on the left.
2026-04-18 23:02:53 +00:00
Claude d34b5702a5 feat(feeds): single grid-level content warning with distinct reasons
Multi-image picture posts now show one warning covering the whole grid
instead of one per image (which required tapping each individually). The
warning backdrop is a grid of the images' blurhashes, and the overlay
lists any distinct reasons collected from both the event's content-warning
tag and per-imeta content-warning fragments as chips. Preloads every
image in the grid while the warning is visible.
2026-04-18 22:59:39 +00:00
Claude 85fcf50df9 feat: warn and offer cleanup for deleted items in pin and bookmark lists
Adds a dismissible top-of-screen banner to the Pinned Notes, Bookmarks,
Old Bookmarks, and Bookmark Set (detail) screens that appears when one
or more listed items have been deleted by their author (kind-5 deletion,
pubkey-matched via DeletionIndex). Tapping "Remove from list" rewrites
the underlying list event (NIP-51 kind 10001/10003/30001/30003) with
the deleted entries stripped out and broadcasts it once.

The scan is scoped to each screen's loaded list (typically <20 items)
and runs only while composed, so it avoids the performance hit of
hooking Account.deletedEventBundles, which fires for every kind-5
deletion in the firehose. Public and private (NIP-44 encrypted)
bookmarks are both cleaned in a single resign() call per list.
2026-04-18 22:58:54 +00:00
Claude 7440f28405 feat(badges): redesign badge composables with Material3 card layout
- Replace the centered, full-width-image RenderBadge with a consistent
  OutlinedCard (12dp corners, 16dp padding) containing a 72dp rounded
  thumbnail, titleMedium name, and a bodyMedium description on
  onSurfaceVariant (max 4 lines).
- BadgeDisplay surfaces an Award button as a FilledTonalButton inside
  the card's action row when the definition is mine.
- RenderBadgeAward now reuses the same card and shows:
  - the badge definition (image + name + description),
  - a compact "Awarded to N" FlowRow of 30dp user pics (capped at 24,
    with an overflow label),
  - a single Accept / Reject action row (TextButton + tonal Accept)
    or an OutlinedButton "Remove from profile" when already accepted.
- Falls back to a robohash thumbnail when no image or thumb is set.
2026-04-18 22:49:02 +00:00
Claude 8555074c3f feat(dvm-favorites): settings page + revert DVM card regression
- Remove FavoriteDvmToggle from DVMCard's title row; the extra
  IconButton was expanding the row and pushing the title down so
  the whole Discover→DVMs card layout looked broken. The toggle
  stays on the DvmContentDiscoveryScreen top bar, which was the
  user's expected place to follow/unfollow.
- Add a "Favourite DVMs" page under Settings (Route.EditFavoriteDvms,
  modelled after the Blossom servers settings) listing each
  favourited content-discovery DVM with its avatar/name/description
  and a delete button. Tapping a row opens the DVM detail screen.
  Empty state points users to Discover for adding more.
2026-04-18 22:48:55 +00:00
Claude 53cec9a661 fix(ui): thread scaffold padding into innermost LazyColumn/Grid
Continues the "scroll-under-bars" migration started for Home/Discover:
the scaffold's PaddingValues is now applied as the inner LazyColumn /
LazyVerticalGrid's contentPadding instead of Modifier.padding(it) on
a wrapping Column or HorizontalPager's contentPadding. That lets
pages/content extend the full height of the screen and scroll behind
the bars, so the bars can hide without leaving the background-colored
strip users were seeing.

Shared infrastructure (opt-in, default PaddingValues(0) so external
callers are unaffected):
- feeds/FeedLoaded.kt adds `scaffoldPadding`
- screen/FeedView.kt (`RefresheableFeedView`, `RenderFeedState`) adds
  `scaffoldPadding` and threads it to the default onLoaded
- feeds/FeedContentStateView.kt (`RefresheableFeedContentStateView`,
  `RenderFeedContentState`) adds `scaffoldPadding`
- screen/UserFeedView.kt (`RefreshingFeedUserFeedView`, `UserFeedView`)
  adds `scaffoldPadding`
- notifications/CardFeedView.kt (`RenderCardFeed`) adds `scaffoldPadding`
  and a `headerContent` slot so NotificationScreen can show its inbox
  relay warning as the LazyColumn's first item instead of a padded
  outer Column
- Per-surface FeedLoaded composables (Shorts, Pictures, Articles,
  Longs, Products) take `scaffoldPadding`

Helper added: `ui/layouts/PaddingMerge.kt` exposes a
`rememberMergedPadding(outer, inner)` so each inner list can combine
the scaffold padding with its own FeedPadding correctly under both
LTR and RTL.

Consumer changes:
- Home, Discover, Polls, Community, FollowPack, OldBookmark (all
  HorizontalPager pages), BookmarkList (pager), Notification, Video,
  WebBookmarks, Drafts, PinnedNotes, Relay, Thread, Hashtag, GeoHash,
  Shorts, Pictures, Articles, Longs, Products: dropped the
  `Modifier.padding(it)` wrapper or the `contentPadding = it` on the
  pager and pass `scaffoldPadding = it` into the feed instead.
- BookmarkListScreen tab row switched to opaque background to match
  the others.

Left unchanged on purpose:
- Settings screens (non-scrollable, bars never hide).
- Chat screens (inverted layout, different interaction model).
- SearchScreen (header-and-feed pattern; migration is larger than this
  pass and is worth its own change).
- FollowPackFeedScreen top bar stays translucent by design.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
2026-04-18 22:42:52 +00:00
Claude f00b7c9b5f fix(badges): use default TopAppBar title font to match other drawer screens 2026-04-18 22:40:50 +00:00
Claude 00ab199e32 fix(feeds): clip + preload content warning on cropped grid cells
- Pass contentScale through to ContentWarningOverBlurhash so grid cells
  (ContentScale.Crop) use fillMaxSize instead of forcing the image's
  native aspect ratio, keeping the blurhash and overlay within the cell.
- Clip the overlay to its bounds to stop the icon/button from drawing
  over neighboring cells or other components.
- Preload the image through Coil while the warning is visible so the
  image is cached by the time the user taps "Show anyway".
2026-04-18 22:36:04 +00:00
Vitor Pamplona aca37b80db Merge pull request #2444 from vitorpamplona/claude/optimize-okhttp-images-N3KJA
Optimize media loading with improved HTTP connection pooling
2026-04-18 18:13:54 -04:00
Claude ee94dba570 fix(home): route DVM listen subscription to DVM relays + wire pull-to-refresh
Two issues from the first pass:

1. Response routing. The kind-5300 request goes to the DVM's own
   inbox/used relays, but `filterHomePostsByDvmIds` was subscribing
   for kind 6300/7000 on the user's outbox/proxy relays. Most DVMs
   only publish their replies on their own relays, so the listen
   subscription would silently miss every response.

   Fix: `Account.requestDVMContentDiscovery` now exposes the relay
   set the request was sent to, the orchestrator stores it on the
   snapshot as `responseRelays`, and `FavoriteDvmTopNavPerRelayFilterSet`
   carries two distinct relay sets — `contentFetches` (user's outbox,
   for the actual notes) and `listenRelays` (DVM's relays, for the
   6300/7000 reply subscription). `filterHomePostsByDvmIds` issues
   each on the right relay.

2. Pull-to-refresh. Swiping down on Home only re-rendered the cached
   feed. When a `TopFilter.FavoriteDvm` is active it now also calls
   `orchestrator.refresh(addr)` so a fresh kind-5300 request is
   published to the DVM.
2026-04-18 21:57:56 +00:00
Claude 843da0b383 fix(home): restrict favourite DVM list to content-discovery DVMs
Only NIP-90 content discovery (kind 5300) DVMs can produce a feed.
Other DVM types (image generation, translation, search, etc.) would
silently hang the home banner waiting for a 6300 reply that will
never come.

Defence in depth:
- FavoriteDvmToggle hides itself when the AppDefinitionEvent doesn't
  advertise kind 5300, so users can't favourite the wrong type.
- TopNavFilterState.mergeInterests filters out any list entry whose
  AppDefinitionEvent doesn't include kind 5300, so a stale or
  cross-client entry won't surface as a Home chip.
2026-04-18 21:27:13 +00:00
Claude af6053f741 feat(nip58): browse, create, award, accept, and edit badges
Adds a top-level Badges destination modeled after Polls, plus the full
create/award/accept/edit lifecycle on top of the existing Quartz NIP-58
event classes.

- Drawer entry + Route.Badges / Route.NewBadge / Route.AwardBadge
- BadgesScreen with 4 tabs (Received / Mine / Awarded / Discover) backed
  by four AdditiveFeedFilters and a new FeedContentState registration.
- BadgesFilterAssembler + BadgesSubAssembler subscribe kinds 30009 and 8
  authored by me; received awards already stream via notifications.
- NewBadgeScreen creates or edits kind 30009 (addressable, so republish
  with same d-tag == edit).
- AwardBadgeScreen takes a list of npub/hex recipients and publishes
  kind 8.
- RenderBadgeAward now shows Accept / Reject / Remove buttons for the
  current awardee, publishing kind 10008 via ProfileBadgesEvent with a
  fallback read of the legacy kind 30008 set.
- BadgeDisplay surfaces an Award action for definitions authored by me.
2026-04-18 21:18:45 +00:00
Claude 6dc723ae2c feat: auto-unpin deleted posts from NIP-51 pin list
When a kind-5 deletion event arrives for a note that is currently pinned,
rewrite the user's PinListEvent to drop the deleted entries and broadcast
the new list. Mirrors the existing deletedNotes() pattern used by
peopleLists, followLists, and labeledBookmarkLists in Account.

Previously the Pinned Notes screen silently hid deleted posts via the
FeedContentState deletion filter, leaving orphan entries in the pin list
that the user had no way to clean up.
2026-04-18 21:07:16 +00:00
Claude 79cb480ea4 perf(media-http): log phase timings and dispatcher queuing
Adds MediaCallEventListener on the images/videos OkHttpClient. For each
call it records DNS, TCP, TLS, TTFB, total time, connection reuse, and
the dispatcher queue depth at call start.

Release builds log only when a call is slow (>= 1.5s), was queued by
the dispatcher (saturation of maxRequests / maxRequestsPerHost), or
failed. Debug builds log every call.

Purpose: give us signal on whether the new 16/host + 128/total limits
are still the ceiling, and whether remaining latency is DNS, TLS, or
server-side.

https://claude.ai/code/session_01PUbqGyUc6oq6V1MdmLi8sw
2026-04-18 20:46:53 +00:00
Claude 9cca64f6ac perf: tune image/video OkHttp dispatcher and connection pool
OkHttp's default dispatcher caps inflight requests per host at 5 and
total inflight at 64. Amethyst feeds typically pull most media from a
single host (e.g. a primary Blossom/imgproxy server), so that per-host
cap serialized feed loading — the browser-feels-faster effect.

- Give the images/videos factory a dedicated Dispatcher (16 per host,
  128 total on device; 5/64 on emulator).
- Give it a larger ConnectionPool (32 idle, 5 min keep-alive) so HTTP/2
  connections to the common media host stay warm across scrolls and
  avoid repeated TLS handshakes.
- Extract isEmulator() to a shared helper used by both the relay and
  image/video factories.

The relay factory is untouched (already tuned for websockets).

https://claude.ai/code/session_01PUbqGyUc6oq6V1MdmLi8sw
2026-04-18 19:34:12 +00:00
Claude 25ecd94487 feat(home): add favourite-DVM top-nav filter
Lets users mark NIP-90 content-discovery DVMs (kind 31990 with k=5300)
as favourite and surface each as a chip in the Home top-nav alongside
hashtags/communities. Selecting a chip publishes the 5300 request,
listens for 6300/7000 responses, and renders the curated feed in
place. A banner above the feed reports processing / payment-required
/ error status and reuses the NWC pay flow extracted from
DvmContentDiscoveryScreen.

- quartz: FavoriteDvmListEvent (NIP-51-style replaceable, kind 10090)
- model: FavoriteDvmListState + backup-on-save + Account mutators
- model: FavoriteDvmOrchestrator for the 5300 request/6300 response
  lifecycle, exposing a per-address StateFlow<Snapshot>
- topNavFeeds/favoriteDvm: TopFilter.FavoriteDvm variant + filter
  classes + FeedFlow wired into FeedTopNavFilterState
- home: FilterHomePostsByDvmIds dispatched by HomeOutboxEventsEoseManager
- UI: FavoriteDvmToggle (star icon on DVM cards + DvmTopBar),
  HomeDvmStatusBanner, new DVMS group and icon in FeedFilterSpinner
2026-04-18 19:12:47 +00:00
Claude 1166654149 refactor(feeds): push content warning into ZoomableContentView per media
Move the blurhash-aware sensitivity warning from feed card wrappers into
ZoomableContentView so every media item shows its own warning sized to
its own aspect ratio over its own blurhash. Grid posts now warn per
image instead of once over the whole grid.

- Add isSensitive to MediaUrl{Image,Video} (defaults to contentWarning != null)
- Populate isSensitive and contentWarning when building media in the
  picture, video, and file-header feeds plus their display variants
- Drop the outer SensitivityWarning wrappers now handled inside
  ZoomableContentView via SensitivityWarningOverBlurhash
2026-04-18 18:51:17 +00:00
Claude f1cbf80826 fix(ui): opaque tab rows and bottom-bar under nav inset
When the disappearing bars translate on top of content, any translucent
chrome lets items bleed through — which the user noticed on Home: posts
became visible through the transparent SecondaryTabRow, and the strip
under Android's gesture bar (covered by windowInsetsPadding on the
bottom nav) was transparent too.

- Swap `containerColor = Color.Transparent` to
  `MaterialTheme.colorScheme.background` on all tab rows that sit in a
  DisappearingScaffold topBar slot (Home, Discover, Polls, Community,
  ChatroomListTabs, OldBookmarkList).
- Paint AppBottomBar's outer Column with the background before the
  windowInsetsPadding, so the system-gesture-bar strip at the bottom is
  opaque and items scrolling behind it are hidden.

FollowPackFeedScreen intentionally uses a translucent (alpha 0.6f) top
bar as a design choice, so it's left as-is.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
2026-04-18 15:59:33 +00:00
Claude 75c96115cb feat(feeds): overlay content warning on blurhash at media size
In the picture and video feeds, render the sensitivity warning over the
media's blurhash placeholder using the media's aspect ratio, instead of
a fixed-size dialog that hides the context entirely. Falls back to the
existing dialog when no blurhash is available.
2026-04-18 15:53:41 +00:00
Vitor Pamplona 8c71d490a7 Merge pull request #2441 from vitorpamplona/claude/marmot-protocol-tests-YZPul
Add Marmot interop test harness for Amethyst ↔ whitenoise-rs
2026-04-18 10:47:47 -04:00
Vitor Pamplona 34d46b08c3 Merge pull request #2442 from vitorpamplona/claude/hide-payment-targets-button-tDYK9
Simplify payment button logic and handle empty targets earlier
2026-04-18 10:28:59 -04:00
Claude 82f5405f01 fix(marmot-interop): robust pubkey extraction from wn output
wn prints either JSON (with --json) or a yaml-ish "key: value" pretty
form depending on the command and build. On the user's macOS run,
create-identity printed yaml ("pubkey: npub1..."), the subsequent
wn --json whoami didn't return the expected JSON shape, and
ensure_identity bailed with "could not determine npub for B".

Add a shared extract_pubkey helper in lib.sh that tries, in order:
  1. JSON object .pubkey / .npub / .public_key
  2. JSON array .[0].pubkey / .[0].npub / .[0].public_key
  3. yaml-ish "pubkey: ..." via sed
  4. yaml-ish "npub: ..." via sed

ensure_identity and prompt_for_a_npub now use it, and also fall back
to the non-JSON whoami output if --json returns nothing. On total
failure, the raw output is echoed to the log for diagnosis.

Also made npub_to_hex best-effort with the same double-try approach.
When hex lookup fails, it returns the npub unchanged — downstream
expect_contains assertions work against either form since wn's own
group listings may use either encoding.

https://claude.ai/code/session_01PacmbE1KKWMSRmDw3bnCNo
2026-04-18 14:27:48 +00:00
Claude 10b229ab9d fix(marmot-interop): don't pass --socket to wnd; derive path from --data-dir
wnd only accepts --data-dir and --logs-dir. The socket path is
{data_dir}/release/wnd.sock (release build) or {data_dir}/dev/wnd.sock
(debug build) per src/cli/config.rs in whitenoise-rs. Updated the
script to point B_SOCKET / C_SOCKET at the correct release path and
dropped --socket from the wnd invocation.

Reported by user running the harness on macOS:
  error: unexpected argument '--socket' found
  Usage: wnd --data-dir <PATH> --logs-dir <PATH>

https://claude.ai/code/session_01PacmbE1KKWMSRmDw3bnCNo
2026-04-18 14:27:48 +00:00
Claude 7107cecedc test: add Marmot interop harness driving whitenoise-rs CLI
Interactive bash script under tools/marmot-interop/ that validates
Amethyst's Marmot/MLS implementation against the whitenoise-rs wn/wnd
CLI. Builds wn/wnd from source on first run, launches two daemons for
Identities B and C, configures public (or --local-relays) Nostr relays
shared with Amethyst, then walks a human operator through 13 scenarios
covering MIP-00 KeyPackages, MIP-01 metadata, MIP-02 Welcome,
MIP-03 group messages, admin changes, reactions, concurrent-commit race,
leave, offline catch-up, and KeyPackage rotation. Push-notification
(MIP-05) test opt-in via --transponder.

No Amethyst code is modified — black-box testing only. State (daemon
sockets, logs, whitenoise-rs checkout, results TSVs) is kept under
tools/marmot-interop/state/ and gitignored.

https://claude.ai/code/session_01PacmbE1KKWMSRmDw3bnCNo
2026-04-18 14:27:47 +00:00