- RadialRenderer: guard minDim <= 0 so a transient 0-size canvas never builds Brush.radialGradient(radius = 0f), which throws.
- AuroraRenderer: clamp y with coerceIn(half, maxOf(half, h - half))
- PcmTapRegistry: never evict a flow that a composable is still collecting (subscriptionCount > 0)
- Aurora/Waves: build the palette-only gradient brushes once via remember(palette)
- audioVisualizerHeight: clamp the fallback strip to min(fallback, maxHeight)
- normalizeToPeakInPlace gains a fromIndex param
- Remove unused silentSpectrum()
feat(audio): wire audio-visualizer settings into navigation and menu
feat(audio): add audio-visualizer settings screen with live previews
feat(audio): add audio-visualizer settings strings
fix(audio): move visualizer setting to Account section; render at feed size
feat(audio): add Classic (default) and Static visualiser styles
feat(audio): show selected live visualiser for audio notes
feat(audio): add change/read accessors for audio-visualizer preference
feat(audio): expose synced audio-visualizer preference flow
feat(audio): add media prefs to synced-settings internal model
fix(audio): thread-safe tap registry, reset spectrum on track reuse
feat(audio): tap decoded PCM via TeeAudioProcessor in pooled players
test(audio): unit-test PCM sink with synthetic sine waves
feat(audio): add PCM-tap registry and FFT audio-buffer sink
fix(audio): continuous viz clock, safe peak-normalize, OFF layout, palette guards
feat(audio): add AudioVisualizer dispatcher composable
feat(audio): add renderer interface, canvas scaffold, registry, and all five styles
feat(audio): add deterministic synthetic spectrum for previews
feat(audio): add VisualizerStyle enum and palette
refactor(audio): drop Visualizer FFT helper, add peak normalization
feat(audio): add Hann windowing + PCM-to-float conversion
feat(audio): add pure-Kotlin radix-2 FFT for the visualiser
- Expose a nullable descriptor
- Log the JSON element kind instead of the raw, network-sourced value.
- drop birthday happy-path tests duplicated by UpdateMetadataTest
- Make birdex_species_preview_more a <plurals> keyed on the remaining count
- Bound the species preview with maxLines=2
- Hoist the joined-names remember out of the conditional (stable slot).
- Drop the unused accountViewModel parameter
- BirdexEvent.speciesCount() derives from speciesNames().size instead of re-scanning tags
- remember() the joined species-name string so it is not rebuilt on every recomposition.
Two correctness bugs in `RemoteSignerManager` (NIP-46) and its NIP-55
sibling `IntentRequestManager`:
1. **Double-resume crash** — `awaitingRequests.get(id)?.resume(value)`
was non-atomic. Multi-relay delivery, bunker echo/retry, and
late-after-timeout responses could call `resume` twice for the same
continuation, throwing `IllegalStateException: Already resumed` on a
`Dispatchers.Default` worker.
2. **Retry id-reuse → wrong data** (NIP-46 only) —
`launchWaitAndParse` built the request and event once, then re-used
the same `request.id` across retry attempts. A late response from
attempt N could resume attempt N+1's continuation with stale data.
Replace the cached-`Continuation` map with the in-house Channel-per-request
correlation pattern already used in `quartz/.../accessories/NostrClientPublishExt.kt`
(`LargeCache<id, Channel<Response>(capacity=1)>` + atomic `remove` +
`trySend` + `withTimeoutOrNull { receive() }`). Each retry attempt now
builds a fresh request with a new id; the builder is still called only
once. `finally`-block cleanup removes the cache entry on every path,
incidentally fixing a slow leak on the success path.
Adds three regression tests:
- duplicate responses → no crash + single resume (fails on \`main\`
with \`IllegalStateException\`)
- late response after timeout → silently discarded
- late attempt-1 response does not corrupt attempt-2 result (fails on
\`main\`: the two attempts share an id)
Design + review notes: \`quartz/plans/2026-06-03-fix-nip46-bunker-double-resume-plan.md\`
On the short new note screen, polls and zap polls kept their option text in
two separate maps (`pollOptions` vs `zapPollOptions`). Switching the poll type
only toggled the `wantsPoll`/`wantsZapPoll` flags, so the text typed for one
type was hidden and never reused for the other.
Make both poll types read and write the same `pollOptions` text fields, so
switching between zap and non-zap polls preserves whatever the user already
typed. The zap-specific settings (deadline, vote value range, consensus
threshold) remain independent.
Two follow-ups to the reply-context PR.
1) Parent-author metadata wasn't reaching the embed / "Replying to @X"
label, so they rendered the truncated hex indefinitely.
- FeedScreen.missingNoteIds: also fetch the immediate parent EVENT
for visible replies (was only repost originals + bech32 quotes).
- FeedScreen.missingAuthorPubkeys: also include the parent AUTHOR
hex, extracted DIRECTLY from each reply's tags
(CommentEvent.replyAuthor() for NIP-22; taggedUsers().lastOrNull()
for NIP-10) so the kind 0 request fires even before the parent
event itself arrives in cache.
- NoteCard.QuotedNoteEmbed + FeedScreen.rememberReplyContext:
produceState observation of the parent author's
metadata().flow so the embed and label recompose to display name
+ avatar once kind 0 lands.
2) Embedded parent appeared clickable but did nothing — the outer
NoteCard's OutlinedCard onClick was catching the click and
re-navigating to the reply's own thread (the current view). Make
the wrapping Box itself clickable, route it to the parent thread,
and drop the inner OutlinedCard's onClick so there's a single
explicit click surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RichTextParser splits each paragraph on ' ' so every segment is one
space-delimited token; the source space lives BETWEEN segments, not
within them. When a paragraph contains only RegularTextSegments the
parser collapses them back to one segment rejoined with " ". When the
paragraph also contains a mention/hashtag/link the segments stay split
and DesktopRichTextViewer rendered them in a FlowRow with no horizontal
gap — every word glued together.
Set the FlowRow's horizontalArrangement to Arrangement.spacedBy(4.dp)
(the same constant the file already uses for ImageGalleryParagraph),
preserving the RTL alignment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Replies tab predicate used `!note.isNewThread()`, which returns true
whenever Note.replyTo is non-empty. The cache populates replyTo from
event.tagsWithoutCitations(), and that includes unmarked positional
NIP-10 e-tags — which modern clients use for QUOTES and MENTIONS, not
replies. Posts that merely quoted another note were therefore appearing
in the Replies tab.
Tighten the signal: a reply is now either a NIP-22 CommentEvent, or a
NIP-10 TextNoteEvent carrying an explicit `reply`/`root` marker tag
(`markedReply()` / `markedRoot()`). Unmarked e-tags no longer qualify.
Adds 6 regression tests including the unmarked-e-tag false-positive
case the user reported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dedicated "Replies" tab between Notes and Reads on the desktop
profile screen so the reply-context rendering can be eyeballed on a
specific user's profile without scroll-hunting for an organic reply.
- DesktopProfileFeedFilter gains a repliesOnly: Boolean = false ctor
param. Default keeps Notes-tab behavior unchanged; when true, the
predicate becomes `event is TextNoteEvent && !note.isNewThread()`
(excludes reposts and chat-message kinds in one check).
- UserProfileScreen: second DesktopFeedViewModel for the replies feed,
new tab at index 1, body branch mirroring the Notes Loading/Empty/
Error/Loaded states. Reads/Gallery/Highlights indices shift by 1.
NIP-22 kind 1111 deferred — most replies today are kind 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detect NIP-10 / NIP-22 replies in the desktop feed pipeline and render an
embedded parent card plus a "Replying to @displayName" label above the
reply body, matching Android's home-feed behavior. Extracts the shared
ReplyToLabel composable + ReplyContext data class to commons so Android
switches over to the shared version.
- commons/.../ui/note/ReplyContext.kt: data class + from(event, cache)
detection. NIP-10 + NIP-22 unified via BaseThreadedEvent polymorphism.
- commons/.../ui/note/ReplyToLabel.kt: shared composable.
- commons/strings.xml: new "Notes & Replies" section + replying_to key.
- desktopApp NoteCard: replyContext param + render branch (bordered
QuotedNoteEmbed + ReplyToLabel). Recursion impossible because
QuotedNoteEmbed's inner NoteCard call doesn't pass replyContext.
- desktopApp FeedScreen: rememberReplyContext() observes parent
metadata flow so embed/label pop in once the parent arrives via
relay subscription. Wired into both regular and reposted-inner paths.
- amethyst ReplyInformation.kt: removed local ReplyToLabel definition.
- amethyst Text.kt: calls shared commons ReplyToLabel; resolves author
display name at the call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Agora (a crowdfunding client on the Ditto stack) publishes fundraising
campaigns as kind 33863 — an app-specific addressable kind with no NIP.
Amethyst had no parser or renderer, so it hit the "Event Not Supported"
path and was dropped; reposts of one rendered as a permanently blank card.
Add first-class support, modelled on NIP-99 Classifieds (title/image/body)
plus NIP-75 zap goals (goal/deadline/progress).
Move isRenderableRepost() (and its test) from amethyst ui/dal into
commons/ui/feeds so both platforms use one implementation, then point
desktop's isFeedNote() at it.