Commit Graph
15230 Commits
Author SHA1 Message Date
nrobi144andClaude Opus 4.6 cc698c5e91 fix(desktop): sidebar starts expanded + extra 16dp feed margin
- Sidebar always starts expanded (ignores persisted collapsed state)
- Feed spacer 60dp → 76dp (extra 16dp gap between header card and first item)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:52 +03:00
nrobi144andClaude Opus 4.6 0e431d674c feat(desktop): branded loading screen instead of login flash on startup
- Add AccountState.Loading as initial state (was LoggedOut)
- Show centered "Amethyst" + spinner while accounts load from storage
- After loadSavedAccount(): transition to LoggedIn or LoggedOut
- No more 0.5s flash of login screen when account exists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:52 +03:00
nrobi144andClaude Opus 4.6 da0aaaa281 fix(desktop): wider collapsed sidebar + filled Tor icon when connected
- COLLAPSED_WIDTH 56dp → 64dp so icons aren't truncated
- Tor connected: use Security icon (filled shield) instead of Shield
- Tor off/connecting/error: keep outlined Shield icon

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:52 +03:00
nrobi144andClaude Opus 4.6 12918e7045 feat(desktop): Tor/Bunker status as proper sidebar nav items
- Replace compact icon Row at bottom with SidebarNavItem-style items
- Tor: shows "Tor: Off/Connecting/Connected/Error" with Shield icon
- Bunker: shows "Bunker: OK" with Favorite icon (only when connected)
- Both use same shape, hover, and label pattern as other sidebar items
- Collapsed mode: icon-only with tooltip, same as nav items

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:52 +03:00
nrobi144andClaude Opus 4.6 7ff4d8e3b7 fix(desktop): avatar ripple + P3 hardcoded colors + inline shapes
Avatar/Account switcher:
- Rewrite SidebarAccountHeader with same shape/hover as other nav items
- Entire row (avatar + display name) is clickable with rounded clip
- Inline DropdownMenu replaces overlaid AccountSwitcherDropdown
- Collapsed: compact avatar with same rounded hover treatment

P3 #004 — Hardcoded status colors:
- Add StatusGreen/StatusRed/StatusAmber to commons Colors.kt
- Replace 32 inline Color() values across 10 files with theme tokens
- Color.Red → MaterialTheme.colorScheme.error where appropriate
- Color.Green/Gray → StatusGreen/onSurfaceVariant

P3 #005 — Inline shapes:
- RoundedCornerShape(8.dp) → MaterialTheme.shapes.small (~20 files)
- RoundedCornerShape(12.dp) → MaterialTheme.shapes.medium
- RoundedCornerShape(16.dp) → MaterialTheme.shapes.large
- Pill shapes (100dp/999dp) kept as-is

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:52 +03:00
nrobi144andClaude Opus 4.6 41a59a9499 fix(desktop): wire Open Full Search via LocalOpenFullSearch CompositionLocal
- Add LocalOpenFullSearch CompositionLocal (navigates to Search column)
- Provided at Main.kt level alongside LocalFeedSearchActive
- FeedScreen reads it directly — no param threading needed
- "Open full search" link in expanded header now actually opens Search column
- Removed unused onOpenSearch param from DeckColumnContainer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:52 +03:00
nrobi144andClaude Opus 4.6 b8f6751a68 fix(desktop): address P2 todos — feed margin + history click
Todo #001: Feed content margin under search
- Animate spacer height: 60dp collapsed → 300dp expanded (tween 200ms)
- Feed items no longer hidden by expanded search card

Todo #002: History item click populates input
- SearchHistorySection gains onHistoryItemClick callback
- Clicking recent search populates searchText with query text
- LaunchedEffect(searchText.text) triggers updateFromText() → relay search

Todo #003: Cmd+F context (deferred — P2 stays open)
- Currently Cmd+F toggles inline search on feeds only
- On non-feed screens it's a no-op (user can click Search in sidebar)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:52 +03:00
nrobi144andClaude Opus 4.6 c7483a4f0d fix(desktop): SearchPill hover bounds + AccountSwitcher ripple size
- SearchPill: move hoverHighlight() inside Surface content Row so it's
  clipped to the pill's 36dp height (was drawing on parent Row height)
- AccountSwitcherDropdown: reduce IconButton from 48dp to 40dp so ripple
  circle fits within collapsed sidebar width

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:51 +03:00
nrobi144andClaude Opus 4.6 c53c8238fb fix(desktop): theming + modifier review fixes from multi-agent analysis
Shapes:
- DeckSidebar: RoundedCornerShape(8.dp) → MaterialTheme.shapes.small
- NoteCard images: RoundedCornerShape(8.dp) → MaterialTheme.shapes.small

Modifier patterns:
- NoteCard: remove hardcoded .fillMaxWidth() — callers now pass it
- FeedScreen NoteCard callers: add explicit Modifier.fillMaxWidth()

Ripple clipping (from prior commit):
- SearchPill: clip(pill shape) before hoverHighlight()
- Sidebar items: clip before clickable
- ColumnHeader: padding before pointerInput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:51 +03:00
nrobi144andClaude Opus 4.6 55704d520b fix(desktop): ripple/hover clipping — correct modifier ordering
- SidebarNavItem/SidebarFeedItem: .clip().clickable().background()
  — ripple now clipped to RoundedCornerShape(8dp) bounds
- ColumnHeader: .padding() before .pointerInput() — gesture detection
  respects horizontal padding
- SearchPill: .clip(pill shape) before .hoverHighlight() — hover
  drawBehind rect clipped to pill shape, not parent rectangle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:51 +03:00
nrobi144andClaude Opus 4.6 5e128cb535 fix(desktop): clear search scrim when navigating via sidebar
Sidebar onNavigate callback now sets searchActiveState.value = false,
clearing the feed search expansion and sidebar dim overlay when the
user clicks any nav item (Home, Messages, Settings, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:51 +03:00
nrobi144andClaude Opus 4.6 971d20be77 fix(desktop): revert relay debounce to 300ms, separate 1s history save
- Relay query debounce stays at 300ms (AdvancedSearchBarState default)
- Separate 1s debounce on searchText: when user stops typing for 1s,
  save the query to SearchHistoryStore (assumes intent confirmed)
- LaunchedEffect(searchText.text) auto-cancels on each keystroke,
  so only fires after 1s of inactivity
- No duplicates: addToHistory() deduplicates by serialized query

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:51 +03:00
nrobi144andClaude Opus 4.6 956eee67d7 feat(desktop): 1s debounce + auto-save search history on collapse
- Increase relay query debounce from 300ms to 1000ms for inline search
  (reduces unnecessary relay load while typing)
- Save query to SearchHistoryStore when search collapses (if non-empty)
- SearchHistoryStore.addToHistory() already deduplicates by serialized query

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:51 +03:00
nrobi144andClaude Opus 4.6 28e8fc627e feat(desktop): polished inline search states — loading, empty, streaming
- LinearProgressIndicator at top of expanded card (animated in/out)
- Loading state: centered icon + "Searching N relays..."
- Empty state: "No results found" / "No search relays configured"
- Results stream in incrementally from relays
- 1s debounce for relay queries, save to search history (no dupes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:51 +03:00
nrobi144andClaude Opus 4.6 e9756d41aa fix(desktop): add loading indicator + relay diagnostics for inline search
- Show "Searching N relays..." with spinner while waiting for results
- Show "No search relays configured" if searchRelays is empty
- Observe isSearching, peopleResults, noteResults directly for state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:51 +03:00
nrobi144andClaude Opus 4.6 e587bb946e feat(desktop): wire relay subscriptions for inline search results
FeedTabsHeader now creates actual NIP-50 relay subscriptions when
typing in the inline search, matching the full SearchScreen wiring:

- Collect debouncedQuery (300ms) from AdvancedSearchBarState
- Access searchRelays from LocalRelayCategories
- rememberSubscription for people search (MetadataEvent kind 0)
- rememberSubscription for note search (SearchFilterFactory filters)
- Results flow into SearchResultsList (reused from search/ package)
- 10s timeout for silent relays
- Subscriptions auto-cleanup on collapse via DisposableEffect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:50 +03:00
nrobi144andClaude Opus 4.6 02679cc596 feat(desktop): animated search expand, tab-collapse, reuse SearchResultsList
1. AnimatedVisibility for search card expand/collapse (expandVertically
   + fadeIn 200ms, shrinkVertically + fadeOut 150ms)
2. Feed tab clicks now collapse the search (onSearchExpandedChange(false)
   before switching feed)
3. When typing in search: reuses SearchResultsList composable from
   search/ package with AdvancedSearchBarState (same as full SearchScreen)
   - Empty input shows search history (recent + saved)
   - Typing shows live people + note results grouped by kind
   - "Open full search" link at bottom for advanced filtering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:50 +03:00
nrobi144andClaude Opus 4.6 bec8fd81ea fix(desktop): search card floats above scrim, sidebar dims separately
- Header card pulled OUT of ReadingColumn into outer Box layer 3
  (rendered after scrim, so it floats visually above it)
- Feed content in layer 1 with spacer for header height
- Scrim in layer 2 covers only feed content
- Sidebar gets its own scrim overlay via Box wrapper in Main.kt
- Search card stays sharp/visible while everything else dims

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:50 +03:00
nrobi144andClaude Opus 4.6 c8e9a4fa6a fix(desktop): search card extends downward, tabs always visible
- Feed tabs + compose button always visible (never hidden)
- Search pill becomes active input when focused, card extends downward
  with history section below the always-visible header row
- Scrim only covers feed content below, header stays sharp via zIndex(10f)
- Cmd+F focuses search, Escape or click-scrim dismisses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:50 +03:00
nrobi144andClaude Opus 4.6 ec8fbd3980 feat(desktop): full-width search expansion with Cmd+F via CompositionLocal
- FeedTabsHeader expands to full-width search card when active
  - Feed tabs hidden, search input fills entire header width
  - History section (recent + saved) shown below input
  - "Open full search" link at bottom
- Scrim overlay dims feed content when search is active
- Cmd+F toggles search via LocalFeedSearchActive CompositionLocal
  - MutableState<Boolean> provided at Window level
  - FeedScreen reads it directly, no param threading needed
- SearchPill simplified back to clickable-only (no inline expansion)
- Escape or click scrim dismisses search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:50 +03:00
nrobi144andClaude Opus 4.6 c69c4462b9 fix(desktop): search pill visibility, sidebar + Add Feed button
- SearchPill: fix collapsed state not rendering — removed nested
  Box/Surface/matchParentSize, replaced with simple if/else on
  expanded state within single Surface+Row
- Sidebar: add "+ Add Feed" item below custom feeds in FEEDS section
- Dropdown menu moves outside Surface to render properly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:50 +03:00
nrobi144andClaude Opus 4.6 55e7fcb903 fix(desktop): feed header card design, inline search, compact tabs
- FeedTabsHeader wrapped in Surface card (border + rounded corners)
- Feed tabs compact with spacedBy(4dp), removed "+ More" (in sidebar)
- SearchPill centered with weight(1f), expands inline to BasicTextField
  with DropdownMenu showing recent + saved searches
- Removed Dialog-based SearchSpotlight (wrong UX — created separate AWT window)
- Cmd+F now navigates to Search column via navigateToScreen callback
- All feed tab types (Following/Global/Custom) properly handled in onClick

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:49 +03:00
nrobi144andClaude Opus 4.6 c21b85c83e feat(desktop): add search spotlight overlay with Cmd+F shortcut
- SearchSpotlight: global overlay with scrim, auto-focused input, recent/saved
  searches, hashtag quick-jump, and "Open full search" link
- SearchPill: compact reusable pill component with hover highlight and shortcut hint
- Cmd+F keyboard shortcut wired in MenuBar to open/close spotlight
- SearchPill added to FeedTabsHeader (Home/Feeds column headers)
- Result selection dispatches to deckState (deck mode) or singlePaneState (single-pane)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:49 +03:00
nrobi144andClaude Opus 4.6 eb114f0a58 refactor(desktop): share MainSidebar across single-pane and deck modes
- Rename DeckSidebar -> MainSidebar (it's not deck-specific anymore)
- Hoist MainSidebar from DECK-only to shared Row in Main.kt
- Remove inline 80dp NavigationRail from SinglePaneLayout
- SinglePaneLayout now only renders content pane (sidebar is external)
- onNavigate dispatches to singlePaneState.navigate() or deckState.addColumn()
  based on active layoutMode
- Active item highlighting works in both modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:49 +03:00
nrobi144andClaude Opus 4.6 b431c1efab feat(desktop): visual personality overhaul — unified theme, sidebar, cards
Phase 1: Replace per-OS color schemes with unified Amethyst brand
- Cyan/blue accent (#0096FF light, #4DB8FF dark) replacing OS-adaptive colors
- Amethyst purple as tertiary heritage color
- Unified shapes (8/12/16/24dp) replacing per-OS variants
- Standardized typography weights (Light for display, SemiBold for headlines)
- Letter spacing unified to -0.3sp

Phase 2: Spacing system
- AmethystSpacing CompositionLocal with design tokens
- LocalIsDarkTheme for M3-compatible dark mode detection

Phase 3: Sidebar redesign
- 240dp wide sidebar with icon + text labels (was 56dp icon-only)
- Animated collapse/expand with smooth width transition
- Avatar + username at top with account switcher
- Custom feeds section from FeedDefinitionRepository
- Active item cyan pill indicator with hover effects
- Collapse state persisted in Preferences
- Debounced fitColumnsToWidth to prevent animation thrash

Phase 4: Card refinement
- OutlinedCard with 1dp border replacing 1dp shadow elevation
- 16dp internal padding (was 12dp)
- Converted NoteCard, ReadsScreen, DraftsScreen, MyHighlightsScreen, UserProfileScreen

Phase 5: Column header restyling
- 48dp height (was 40dp) with surfaceContainer background
- 12dp horizontal padding (was 8dp)

Phase 6: Polish
- HoverModifiers.kt — shared hover highlight using onPointerEvent + drawBehind
- ShimmerPlaceholder.kt — skeleton loading animation in commons/commonMain

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:49 +03:00
Vitor PamplonaandGitHub 16afed0f3f Merge pull request #3097 from vitorpamplona/claude/intelligent-heisenberg-MkXpX
Fix scroll position restoration in feed lists on return
2026-05-28 21:28:52 -04:00
Claude f8ff9049d9 fix(onchain): highlight bolt for own onchain zaps + own pending in counter
Two parallel gaps to the cashu work, surfaced once the cashu side
was wired correctly:

1. The orange bolt highlight on the reaction row never lit up for
   onchain zaps. Note.isZappedBy checked LN zaps, NWC payments,
   and (since Phase 1) nutzaps — but never onchainZaps. And the
   fast-path gate in ObserveZapIconState shared the same blind
   spot. Add isOnchainZappedBy parallel to isNutzappedBy (same
   shape: any onchainZaps entry whose source.author matches the
   user and whose source event is newer than afterTimeInSeconds),
   and extend the gate with onchainZaps?.isNotEmpty().

2. The reaction-row counter included CONFIRMED onchain amounts
   via updateZapTotal (verifiedSats only, per NIP-BC) but not
   the signed-in user's OWN pending/unverified outgoing zaps.
   That created a UX mismatch: the gallery shows the user's own
   UNVERIFIED entry with its claimed sat amount immediately
   (the user knows what they sent), but the counter stays at 0
   until the chain catches up. Add
   Note.extraOwnPendingOnchainSats(loggedInPubKey) that sums
   claimedSats from non-CONFIRMED onchainZaps whose source.author
   matches the logged-in pubkey, and add it on top of zapsAmount
   in both AccountViewModel.calculateZapAmount paths and
   ObserveZapAmountText's no-zapPayments fast path. Other senders'
   non-confirmed entries still contribute 0, preserving the
   anti-spoof posture for incoming zaps.
2026-05-28 23:06:10 +00:00
Claude 02fd0780d5 fix(feeds): don't snap to top when returning from a post screen
StickToTopOnPrepend hardcoded `wasAtTop = true` on remember, so when the
user scrolled down, navigated to a post and came back,
rememberForeverLazyListState restored a non-zero offset but the helper
still believed the user was at the top. As soon as the head-of-feed key
emitted (first observation after recomposition), the LaunchedEffect
scrolled them back to 0.

Seed wasAtTop from the actual restored scroll position via an
initialAtTop lambda. The sampler's gesture-guarded false→true protection
against keyed-item shifts is unchanged.

Regression from #3088.
2026-05-28 23:01:49 +00:00
Claude aeadbbd276 Make TimeAgoFormatter + CalendarTimeFormat thread-safe
The module-level mutable SimpleDateFormat formatters in these files
were read concurrently — UI composition on the main thread, and
LocalCache.justVerify calling dateFormatter() from background event-
verification coroutines for failed-signature log lines. SimpleDateFormat
is not thread-safe (mutable internal Calendar), and updateFormattersIfNeeded
reassigned the field mid-format. Race produced corrupted timestamp
strings and occasionally NumberFormatException inside format().

Replace the shared-var pattern with a small LocaleAwareFormatter that
wraps a ThreadLocal<Pair<Locale, SimpleDateFormat>>. Each thread caches
its own instance and rebuilds lazily when Locale.getDefault() changes —
no locks, no contention, same allocation profile after warm-up.

Apply the same pattern to CalendarTimeFormat for consistency; today its
callers are all main-thread but the structure was identical.
2026-05-28 23:00:03 +00:00
Vitor PamplonaandGitHub ba049da389 Merge pull request #3096 from vitorpamplona/claude/busy-sagan-QqTna
Switch Java distribution from Zulu to Temurin
2026-05-28 18:58:06 -04:00
Claude 6312c24f2f fix(nutzap): orange highlight on the lightning bolt for cashu zaps
The reaction-row icon turns orange when the logged-in user has
zapped a note. Phase 1 extended Note.isZappedBy to detect cashu
zaps, but ObserveZapIconState had a fast-path gate that skipped
the whole calculation when the note had no LN zap data:

    val hasZapData =
        zapsState?.note?.zapPayments?.isNotEmpty() == true ||
            zapsState?.note?.zaps?.isNotEmpty() == true

A cashu-only zap leaves both `zaps` and `zapPayments` empty, so
hasZapData was false, wasZapped was hard-coded false, and the
new isZappedBy logic never ran. The amount counter still updated
because that path (Note.zapsAmount) was already extended in
Phase 1 — only the icon gate was missing the third source.

Add `nutzaps?.isNotEmpty()` to the gate.
2026-05-28 22:57:12 +00:00
Claude d174e054e0 fix(nutzap): smaller gallery icon + drop redundant success toast
Two UI follow-ups after seeing the rails in the running app:

1. ReactionDetailGallery cashu row was Size25dp, matching the
   lightning-bolt convention from OnchainZapGallery. The
   multi-tone cashu glyph reads bigger than a thin bolt at the
   same nominal dp, so the row felt visually heavier than the
   lightning row above it. Drop to Size20dp to match the cashu
   chip in MultiSetCompose.RenderNutzapGallery and the boost /
   like rows that sit below.

2. sendNutzap's success toast is now redundant. The earlier
   Phase 1 work attaches the kind:9321 to the target Note via
   addNutzap, so the reaction-row counter and the
   you-already-zapped icon highlight both update on their own
   the moment the kind:9321 round-trips through the cache. A
   "Cashu zap sent" toast on top of that visible state change
   is just noise. Drop the toast and the now-unused
   nutzap_sent_title / nutzap_sent_amount strings.

The Phase 0 work was an artifact of the order I implemented the
phases — feedback first while the counter was still LN-only, then
the foundation made the toast moot. Removing it now keeps the
final UX clean.
2026-05-28 22:53:10 +00:00
Claude c63c48a778 Cache CalendarTimeFormat formatters at module scope
The post-refactor `bestPattern(skeleton)` helper allocated a fresh
SimpleDateFormat — plus ran the ICU lookup in `getBestDateTimePattern`
— on every call. That's hot in the calendar grid: 7 weekday-header
calls per render and up to 42 `formatLongDate` cells per month render.

Restore the old caching pattern (module-level vars, rebuild on locale
change via `refreshFormattersIfLocaleChanged`), now keyed off the
locale-aware skeleton patterns. Time format still re-fetched per call
via `DateFormat.getTimeFormat(context)` since the user's 24-hour
override can change without a locale change.
2026-05-28 22:47:55 +00:00
Claude cbd8cd1b87 ci: switch setup-java distribution from zulu to temurin
Zulu's CDN (cdn.azul.com) has been returning HTTP 520 for several
hours, breaking JDK 21 setup across multiple PRs (#3075, #3095) on
every Linux runner. actions/setup-java's internal retry budget isn't
enough to ride through it.

Temurin (Adoptium) publishes its JDKs as GitHub Releases assets, so
the download path uses GitHub's own CDN — completely independent
infrastructure. Java 21 is the same upstream OpenJDK build either way,
so Gradle, Compose Multiplatform, and jpackage behave identically.

Applied to all setup-java steps in build.yml, smoke-test-desktop.yml,
and create-release.yml.
2026-05-28 22:46:29 +00:00
Claude 39f9dd334a feat(nutzap): dedicated cashu row in ReactionDetailGallery
Phase 4 of the nutzap UX integration. The expanded reaction view
on a note now shows a cashu row between the lightning row and the
onchain row, matching the rail visual hierarchy used elsewhere
(zap chip popup → notifications card → reaction-detail gallery).

New `NutzapGallery.kt` modelled on `OnchainZapGallery.kt`:

- `WatchNutzapsAndRenderGallery` subscribes to the same per-note
  zap flow the lightning + onchain galleries already use. Memoizes
  on the nutzaps map reference (immutable per mutation) so a
  lightning zap arrival on the same note doesn't recompose this
  row, and renders only when at least one entry exists.

- Cashu icon (CustomHashTagIcons.Cashu, tint=Unspecified) followed
  by a FlowRow of sender avatars with the claimed sat amount
  overlay. Unlike onchain, there's no UNVERIFIED/PENDING gating —
  every entry shows at full opacity because nutzap verification
  is the recipient wallet's job at redeem time (the
  reaction-detail gallery is just showing "who sent what to this
  note").

- Tapping an entry navigates to the sender's profile, same as the
  other galleries.

Wired into `ReactionsRow.ReactionDetailGallery` between
`WatchZapAndRenderGallery` and `WatchOnchainZapsAndRenderGallery`,
so the three zap rails read top-to-bottom in the order they tend
to surface for a given recipient (LN cheapest/easiest, cashu
mint-mediated, onchain final-settlement).

That closes the 4-phase nutzap UX work:
- Phase 0: success toast on send (sendNutzap previously silent)
- Phase 1: Note.nutzaps + LocalCache wiring; reaction-row counter
  and icon highlight come along for free via zapsAmount and
  isZappedBy extension.
- Phase 3: notifications — NotificationFeedFilter kind add,
  NutzapUserSetCard for per-sender aggregates, cashu rail in
  MultiSetCard, NutzapUserSetCompose card renderer.
- Phase 4 (this): dedicated cashu row in the expanded reaction
  view.
2026-05-28 22:37:20 +00:00
Claude ef8f49733a feat(nutzap): notifications card surface
Phase 3 of the nutzap UX integration. Inbound NIP-61 nutzaps
(kind 9321) now appear in the notifications feed alongside
lightning zaps, boosts, likes, and onchain zaps — but with the
cashu icon so the rail is visible at a glance.

Pieces:

- NotificationFeedFilter.NOTIFICATION_KINDS now includes
  NutzapEvent.KIND. The existing tagsAnEventByUser logic falls
  through to its `return true` default for nutzap (it's not a
  BaseNoteEvent / ReactionEvent / Repost / Git / Highlight), and
  the kind:9321's `p` tag carries the recipient so isTaggedUser
  matches.

- CardFeedContentState gains a parallel nutzap grouping pass.
  Nutzaps targeting a specific note feed into nutzapsPerEvent
  and roll into the per-note MultiSetCard; nutzaps without an
  e-tag target (recipient-only) feed into nutzapsPerUser and
  surface as a NutzapUserSetCard per sender per day.

- MultiSetCard extended with `nutzapEvents: ImmutableList<Note>`,
  and its min/max createdAt cover the cashu rail too. Adding a
  new field at the end with a `persistentListOf()` default keeps
  the existing constructor sites compatible.

- New NutzapUserSetCard for the per-sender aggregate. Wraps raw
  kind:9321 Notes (no request/response pair like LN), keyed by
  pubkey+createdAt with an "N" suffix so it never collides with
  the LN ZapUserSetCard.

- MultiSetCompose: new RenderNutzapGallery row renders the cashu
  icon (CustomHashTagIcons.Cashu, tint=Unspecified to preserve
  the brand colour) followed by the same AuthorGalleryZaps the
  lightning rail uses. Each kind:9321 is mapped to a
  ZapAmountCommentNotification with the claimed sat total and
  the event content as the comment.

- New NutzapUserSetCompose modelled on ZapUserSetCompose, also
  wired into CardFeedView's card switch.

The user-facing result: the notifications screen now shows a
"X sent you Y sats via cashu" card with the cashu icon, exactly
the same shape as the lightning version, and the per-note
multi-card grows a cashu rail when the note has cashu zaps.

Phase 4 next: the dedicated cashu line in ReactionDetailGallery,
modelled on the existing onchain row.
2026-05-28 22:35:15 +00:00
Vitor PamplonaandGitHub 18495449f8 Merge pull request #3094 from vitorpamplona/claude/kind-feynman-tZUzp
docs: consolidate CLAUDE.md guidance and inject skills dynamically
2026-05-28 18:26:12 -04:00
Claude 599fa2aa53 docs: restore canonical NIPs link in CLAUDE.md
Re-add the nostr-protocol/nips reference (dropped in the cleanup) as a
one-liner in the overview, tied to the /nip command so it points at the
exact spec file rather than the bare index.
2026-05-28 22:24:33 +00:00
Vitor PamplonaandGitHub 0eb2188ebd Merge pull request #3093 from vitorpamplona/claude/adoring-turing-THM7p
Add podcast support (NIP-F4) with favorites, metadata, and episodes
2026-05-28 18:24:11 -04:00
Claude c679a870ab Respect Android system date/time format preferences
Hardcoded date/time patterns ignored the user's Locale (date order:
dd/mm/yyyy vs mm/dd/yyyy vs yyyy-mm-dd) and the system 12/24-hour
override. Replace them with locale-aware formatters that resolve order
from the active Locale via DateFormat.getBestDateTimePattern() and pick
the time-of-day pattern via DateFormat.is24HourFormat(context).

- TimeAgoFormatter (amethyst + commons): build SimpleDateFormat from
  Unicode LDML skeletons (yMMMd / MMMd / yMMM) so "May 28, 2026" in
  en-US becomes "28 May 2026" in en-GB, "28.05.2026" in de-DE, etc.
- CalendarTimeFormat: same skeleton approach for date pieces; time
  uses DateFormat.getTimeFormat(context) so a 24-hour Android user
  sees 14:32 even on a 12-hour locale.
- New LocalizedDateTimeFormat helper with formatMonthDayTime,
  formatMediumDate, formatMediumDateTime — used by wallet, vanish,
  attestation, namecoin, eventsync screens to replace inline
  SimpleDateFormat("MMM d, HH:mm") / ("MMM dd, yyyy  hh:mm a") etc.
- Material3 TimePicker callers (calendar/nest/poll/zap-poll/expiration
  date pickers + vanish request) now pass is24Hour from the system
  setting instead of hardcoding false.
- Desktop article/reads/highlights screens use
  java.text.DateFormat.getDateInstance(MEDIUM, locale).
- Drop dead formattedDateTime() in RelayCompose (was unused).

Intentionally left alone: notification feed bucket keys ("yyyy-MM-dd"
used as Map keys), TakePicture file naming (Locale.US), iCalendar
RFC 5545 stamps, NIP-52 ISO date storage, internal logging, and
ThreadLevelCalculator sort keys — none are user-facing.
2026-05-28 22:22:59 +00:00
Claude 12ed86627c feat(nutzap): fold nutzaps into reaction-row zap counter + icon highlight
Phase 0 (small fix): sendNutzap was async-launched with no success
callback, so after tapping the teal cashu chip in the zap picker
the popup vanished and the user saw no feedback for the 1-2 seconds
it took the swap + publish to complete. Add a "Cashu zap sent —
Sent N sat(s) via cashu" toast on success, matching the lightning
zap's progress feedback in spirit.

Phase 1 (foundation): NIP-61 nutzaps attach to their target note
the same way LN zaps and onchain zaps do, contributing to the
reaction-row total and the "you-already-zapped" icon highlight
without any UI-layer change.

Pieces:

- NutzapEvent.claimedSatsTotal() in quartz parses the sender-
  claimed sat sum from the proof tags once, leniently (a single
  malformed proof contributes 0 rather than throwing). The
  recipient wallet still verifies proofs against the mint at redeem
  time; this is the trusted-claim total for display.

- Note.nutzaps: Map<HexKey, NutzapEntry> on the canonical commons
  Note, parallel to onchainZaps. NutzapEntry carries the source
  kind:9321 note (sender = source.author) and the pre-parsed
  claimedSats. Volatile because writes happen on applicationIOScope
  and reads happen on the Compose main thread.

- updateZapTotal() now sums nutzap claimedSats into zapsAmount, so
  the existing ObserveZapAmountText composable in ReactionsRow
  picks up cashu without code change.

- hasZapped() and the suspend isZappedBy() extended to detect
  nutzaps from a given user. ReactionsRow's calculateIfNoteWasZap-
  pedByAccount path therefore highlights the bolt orange for cashu
  zaps the same way it does for lightning.

- LocalCache previously routed NutzapEvent through
  consumeRegularEvent, which would add it as a *reply* to the
  e-tagged note via computeReplyTo. computeReplyTo gains a
  NutzapEvent case returning the linked event ids, and a dedicated
  consume(NutzapEvent) function attaches via addNutzap instead of
  addReply.

The "list" merge across LN + cashu + onchain that the user floated
is deferred — three separate collections with different shapes
(zap pair, onchain entry, nutzap entry) are kept; only the
aggregates and queries are unified. That's enough for the
reaction-row UX and avoids touching every iteration site at the
call layer.

Coming next: notifications (NotificationFeedFilter + a cashu-icon
variant of ZapUserSetCard) and the dedicated cashu row in
ReactionDetailGallery modeled on OnchainZapGallery.
2026-05-28 22:20:25 +00:00
Claude ab0719fa68 fix(podcasts): wire podcast kinds into every consumer; close audit findings
Addresses 10 audit findings from the post-build review:

CRITICAL — non-functional without this:
- LocalCache.justConsume had no branches for kind 54 / 10054 / 10064 / 10154,
  so every podcast event fell through to "Event Not Supported" and was
  silently dropped. Added the four explicit branches (regular event for
  PodcastEpisode; replaceable for the other three).

HIGH — silent invisibility / broken tap-through:
- Home, profile (newthreads + mutual), hashtag, geohash, follow-pack, and
  notification feed filters didn't recognize PodcastEpisodeEvent /
  PodcastMetadataEvent. Episodes were invisible everywhere outside the
  dedicated tab; reactions/zaps on episodes were dropped from the
  notifications feed.
- ThreadFeedView's renderer dispatch had no podcast branch, so tapping a
  feed card opened a plain text-note view. Added explicit cases that call
  the new RenderPodcastEpisode / RenderPodcastMetadata composables.
- The hashtag / geohash / relay / search REQ kind lists didn't include
  podcast kinds, so discovery surfaces returned nothing for them.
- RelayInformationScreen kind→label map gained podcast entries +
  4 new string resources (Podcast Episode, Podcast Show, Authored
  Podcasts, Favorite Podcasts).
- HomeNewThreadFeedFilter.ADDRESSABLE_KINDS gained PodcastMetadataEvent so
  shows surface alongside music/wiki/long-form on the home feed.

HIGH — privacy leak in Quartz:
- FavoritePodcastsListEvent.add(isPrivate=true) was passing
  earlierVersion.tags through untouched, so toggling a previously-public
  favorite to private left the public p-tag intact. Made both branches
  symmetric: each removes the entry from the other half before adding to
  its own. Two regression tests cover the round-trip.

MEDIUM — data hygiene:
- AuthorTag.parse used to accept ANY non-empty slot-2 string as a role
  (rendering a stray relay-hint URL as "Role: wss://relay…"). Now
  validates against the spec-defined {host, cohost, editor} allowlist;
  unknown values resolve to role=null, preserving the pubkey association.

PERF:
- PodcastEpisode renderer was allocating a fresh 96-element WaveformData
  and rebuilding the cover Modifier chain per visible card. Hoisted both
  to top-level constants (FLAT_WAVEFORM, COVER_IMAGE_MODIFIER,
  PLAYER_BORDER_MODIFIER) so the whole feed shares one instance.

CODE QUALITY:
- Extracted PodcastCoverCard as a shared composable used by both renderers
  (was duplicated byte-identical across PodcastEpisode + PodcastMetadata).
- Extracted PodcastFeedLoaded so the Episodes screen and Shows screen
  share one feed body (was duplicated byte-identical).
- Dropped the misleading `group = listOf(singleAssembler)` wrapper in the
  two FilterAssembler files.
- Replaced `mapNotNull { … }.flatten()` with `flatMap { … }` in the
  Communities sub-assembly (the lambda never returns null).

DOCUMENTED:
- PODCAST_KINDS "Following" resolution still goes through kind:3 follows,
  but per NIP-F4 podcasts use their own keypairs tracked via kind:10054.
  Added an inline comment naming the deferred work — proper fix needs
  Account-level 10054 integration which is a separate scope.
2026-05-28 22:14:49 +00:00
Claude aea885bab0 docs: add Verify-Don't-Guess rule and trim CLAUDE.md
- Add a standing instruction to test hypotheses before diagnosing
  (state guesses as guesses, reproduce-first, predict-then-run).
- Remove content duplicated by the harness-injected skill list (skills
  tables, Commands section) and generic expect/actual examples.
- Condense the Feature Workflow (removed the duplicated share/keep-native
  tables and the hardcoded grep block) and the skill-handshake example.
- Fix stale facts: drop pinned tool versions (now point to
  libs.versions.toml) and correct the nestsClient tree comment to match
  the overview (production runs on moq-lite).
2026-05-28 22:12:53 +00:00
Vitor PamplonaandGitHub 1fdde747de Merge pull request #3092 from vitorpamplona/claude/adoring-galileo-ws6FG
Add NIP-78 AppDataEvent (kind 78) and refactor AppSpecificDataEvent
2026-05-28 18:08:33 -04:00
Vitor PamplonaandGitHub cb8efd070e Merge pull request #3091 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-28 17:45:59 -04:00
Claude 6c3ceba234 docs: add Verify, Don't Guess standing instruction to CLAUDE.md 2026-05-28 21:41:32 +00:00
Crowdin Bot 47af414e47 New Crowdin translations by GitHub Action 2026-05-28 21:38:48 +00:00
Vitor PamplonaandGitHub a0af93bbbe Merge pull request #3090 from vitorpamplona/claude/exciting-meitner-KeUBf
Add NIP-71 audio-track support with language and bitrate tags
2026-05-28 17:36:56 -04:00
Claude 6a0f78a75c refactor(richtext): drop cashuA/cashuB shortcut in isMarkdown
The intraword `_` rule (CommonMark §6.2, implemented in the
previous commit) is sufficient on its own to keep base64url
cashu payloads out of the markdown renderer — every `_` inside
such a token is intraword and gets skipped. The upfront
`contains("cashuA"/"cashuB")` scan was a safety net for the
mixed-content case ("**enjoy** cashuB..."), but keeping it broke
markdown rendering for any post that legitimately had both
markdown formatting AND a cashu token. Drop it: mixed posts
render markdown correctly, cashu-only posts still skip markdown
via the intraword rule.
2026-05-28 21:27:42 +00:00
Claude 40ed26ea85 refactor(quartz): NIP-78 events use the build-template pattern
Convert both kind 30078 and the new kind 78 from the legacy
`suspend create(... signer)` shape to the now-standard
`build(...) -> EventTemplate` shape used across recent quartz events
(NIP-34, NIP-66, etc):

- Use `eventTemplate<T>(KIND, content, createdAt) { ... }` and lean
  on the shared `alt()` and `dTag()` TagArrayBuilder extensions
  instead of hand-rolling the `d`/`alt` injection.
- Callers now do `signer.sign(AppSpecificDataEvent.build(...))`.

For kind 78 the `d` tag is optional (it's a grouping key, not an
addressing key), so we keep it nullable and assemble it via
`DTag.assemble` — the typed `dTag()` extension is constrained to
addressable events, which is correct.

Update the lone caller (`AppSpecificState.saveNewAppSpecificData`).
2026-05-28 21:16:03 +00:00