Replaces the per-relay backoff-token approach (opaque WebsocketBuilder
config token + BasicRelayClient bookkeeping) with a much smaller check at
the source.
The connector already holds the two OkHttp clients, and those are rebuilt
only when something connection-relevant changes (Tor's SOCKS port appears,
wifi<->cellular switch). Everything else it wakes on — Tor bootstrap status
churn, connectivity blips, self-heal restarts — leaves the clients
untouched. So instead of threading a config token through quartz, the
connector now forces a backoff-skipping reconnect (ignoreRetryDelays=true)
only when an OkHttp client instance actually changed; otherwise it lets
each relay's exponential backoff decide. That stops the
reconnect-fail-reconnect loop while Tor boots, and still reconnects every
relay (Tor and clearnet alike) the instant the transport changes.
Reverts the quartz/OkHttpWebSocket changes from the previous commit and
wires the connector to take the StateFlows it actually consumes (the two
client flows + connectivity/tor status) instead of the manager objects,
which also decouples it from DualHttpClientManagerForRelays/
ConnectivityManager/TorManager.
https://claude.ai/code/session_01SCz8kdYs2FwesEyzbhmRPY
Bump the pixel sunglasses scale (1.45 -> 1.70) so the shades read bolder and
overhang the slimmed, rounded cashew body more prominently. Body outline and
1.2 stroke weight unchanged.
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.
Before Tor is ready, relays were disconnecting and immediately
reconnecting, ignoring BasicRelayClient's exponential backoff.
RelayProxyClientConnector calls reconnect(ignoreRetryDelays = true) on
every infrastructure change. That flag flows pool-wide into
connectAndSyncFiltersIfDisconnected and unconditionally bypassed the
backoff gate. While Tor is still bootstrapping its SOCKS port isn't
listening, so each unrelated infra event (connectivity transitions,
self-heal restarts, Tor status churn) forced an immediate reconnect that
failed again — the backoff was computed (delay kept doubling) but never
consulted.
Make the bypass per-relay and conditional on the relay's transport
config actually changing since its last attempt:
- WebsocketBuilder gains connectionConfig(url): an opaque,
value-comparable token of the transport config (proxy + timeouts).
Default null = untracked, preserving legacy always-bypass behavior for
in-process/standalone/test/desktop builders.
- BasicRelayClient records the token at each attempt and only lets a
forced reconnect skip the backoff when the token changed. A Tor relay
whose SOCKS config is unchanged keeps honoring its backoff; the moment
Tor flips to active (proxy port appears) the token changes and it
reconnects immediately. Clearnet relays still retry immediately
whenever their own client changes.
- OkHttpWebSocket.Builder reports the proxy+timeout fingerprint, reused
by needsReconnect() to avoid drift.
Adds BasicRelayClientBackoffTest covering the honored/bypassed/untracked
cases.
https://claude.ai/code/session_01SCz8kdYs2FwesEyzbhmRPY
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>