Replace the blocky pixel-stepped cashew silhouette with a smooth rounded
outline (corner-cut and emitted as a compact Bézier spline) so the nut reads
as a clean curved shape instead of a staircase. Use a round stroke cap/join at
the same 1.2 weight; the pixel "deal-with-it" sunglasses stay a solid fill.
Squeeze the cashew outline horizontally (~0.72) and scale the pixel sunglasses
up (~1.45) so the shades overhang the body for a bolder, more recognizable
mark. Stroke weight stays at 1.2 to match the shared Zap outline icon.
Convert the multi-tone "deez nuts" Cashu/nutzap logo into a single-color,
tintable outline icon so it behaves like a Material Symbol glyph: the cashew
body is a hollow stroke (1.2 weight, matching the shared Zap outline icon) and
the pixel sunglasses stay a solid fill so they read at small sizes.
Drop the `tint = Color.Unspecified` overrides at every call site (zap chips,
nutzap rows/gallery, redeem, wallet screens) so the icon now tints with the
surrounding content colour instead of being locked to the brand browns, and
remove the imports/comments that only existed to preserve the old multi-tone
rendering.
RenderPodcastMetadata and RenderPodcastEpisode rendered their description
tag as a plain Text. Both now use TranslatableRichTextViewer (rich text +
translation), matching the podcast screen header. The metadata card finally
uses its previously-ignored canPreview/backgroundColor params; the episode
card gives its short description a distinct id so it doesn't share remember
state with the markdown body below it.
- The show description on the podcast screen now renders through
TranslatableRichTextViewer (rich text + translation), matching how a
profile's About is shown, instead of a plain Text. PodcastHeader takes a
nav for the rich content's links.
- Audio playback now shows a decorative per-episode waveform when the
source has none, instead of a flat strip — same behaviour as music
tracks. Extracted MusicTrack's private syntheticWaveformFor into a shared
wavefront/SyntheticWaveform.kt used by both music and podcast players; the
podcast player still prefers a real IMeta waveform when one is present.
Audit follow-ups on the podcast screen:
- Extract the duplicated GetMediaItem -> GetVideoController -> RenderVoicePlayer
block (and the FLAT_WAVEFORM constants) from both PodcastEpisode and
PodcastEpisodeListItem into one PodcastEpisodeAudioPlayer composable. The
border shape stays a parameter (bottom-rounded under the cover vs. fully
rounded in the list).
- Fix the list row clipping the 75dp play button: the row hard-coded 72dp,
below the voice player's 80dp minimum. Height now lives in the shared
player so both call sites stay identical.
- Header no longer flashes "0 episodes" while the relay request is in flight:
episodeCount is nullable and the count/divider only render once loaded.
- Drop the odd uppercasing of the episode date (looked wrong on relative
values like "3d").
Each item in the Podcasts feed (kind 10154 show metadata) now opens a
dedicated podcast screen built around the show's pubkey — per NIP-F4 a
podcast is its own keypair, so its metadata and every episode (kind 54)
share one author.
- New Route.Podcast(pubkey) + AppNavigation binding.
- PodcastScreen: modern hero header (cover art, title, websites, tappable
expand/collapse description, "N episodes" section) followed by every
episode in a LazyColumn.
- PodcastEpisodeListItem: compact "show list" row (date, title, summary)
with an inline audio player so episodes play without leaving the list.
- OnePodcast datasource (per-user EOSE manager) fetches the show metadata
and episodes from the podcast key's outbox relays.
- OnePodcastEpisodesFeedFilter + OnePodcastFeedViewModel scope LocalCache
to a single podcast's episodes.
- Podcasts feed metadata card is now clickable with a "View episodes"
affordance.
The cashu zap chip previously fired its async send and closed the
popup with no visible motion until the kind:9321 round-tripped
back (1-2 seconds of "did the tap register?" silence). Wire the
same zappingProgress channel the lightning path uses so the bar
visibly animates from tap to completion.
Pieces:
- CashuWalletOps.sendNutzap: optional `onProgress: (Float) -> Unit`
emitting at the network checkpoints — 0.55 after the swap (the
long mint round-trip), 0.80 after the kind:9321 publish (the
recipient can see it), 0.95 after keep + delete, 1.0 after the
kind:7376 history.
- CashuWalletState.sendNutzap: forwards onProgress to ops and
also emits 0.20 after scrubLocallyStaleProofs so the bar moves
before the slow swap call begins.
- AccountViewModel.sendNutzap: accepts an `onProgress: (Float) -> Unit`
parameter (default no-op so existing callers compile unchanged)
and threads it through to CashuWalletState.
- ReactionsRow.onNutzap callback: emits 0.05 immediately on tap
for instant click feedback, then passes the same `onProgress`
channel the lightning chip uses so zappingProgress drives the
same progress indicator. The existing onError reset already
zeroes the bar on failure.
Resulting cadence at the UI:
tap → 0.05 (instant)
→ 0.20 (scrub done)
→ 0.55 (mint swap done)
→ 0.80 (kind:9321 out)
→ 0.95 (token rollover + NIP-09 out)
→ 1.00 (history out, bar hides).
The grace-period unsubscribe in LifecycleAwareKeyDataSourceSubscription ran
on the composition scope from rememberCoroutineScope(), whose dispatcher is
coupled to the UI frame clock. When the app is backgrounded the frame clock
stops ticking, so the pending unsubscribe could be starved and never fire.
Because closing the REQ is what drives the relay disconnect (via desiredRelays
-> RelayPool.updatePool), the connection could linger indefinitely. This is
most visible on the relay feed, whose dedicated one-off relay is kept alive by
nothing else.
Drive the grace timer from Lifecycle.currentStateFlow on a dedicated
Dispatchers.Default scope instead. collectLatest cancels the pending delay
automatically when the lifecycle returns to STARTED, preserving the 30s
app-switch grace while ensuring the timer fires reliably in the background.
https://claude.ai/code/session_01SesftJphLwvLtn1fJB5zx8
Opens the App Drawer (same as Cmd+K) for quick access to all
available screen types. Positioned between main nav and FEEDS section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Revert spacer change (back to 60dp)
- Add top = 16.dp to LazyColumn contentPadding so first card has
extra spacing and slides nicely under the floating search header
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
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>
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>
- 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>
- 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>
- 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>
- 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>