The AppImage build inputs (AppRun, .desktop entry, icon, and the
CI-fetched linuxdeploy binary) are consumed only by desktopApp's
createReleaseAppImage task. Co-locating them under
desktopApp/packaging/appimage/ removes the `../` path escape from the
build script and keeps all desktop packaging assets inside the module.
https://claude.ai/code/session_0137ULcfJkASmfmffFBdW8ac
A Comment event (kind 1111) scoped to an external identifier (`I` tag)
previously rendered as a bare text post and landed in the Home "New
Threads" feed. Treat it as a reply to that external scope: it now shows
in the conversations feed and renders a typed chip (hashtag, geohash,
url, or generic) above the comment text.
https://claude.ai/code/session_01ArHkNXu1ANrVGZAyMWg4Xu
ScheduledPostsScreen used a plain Scaffold with no bottom bar and an
unconditional back arrow, so a user who pinned it to the bottom nav got
a back arrow and no bottom bar instead of the standard tab-root shell.
Add AppBottomBar(Route.ScheduledPosts) and gate the back arrow on
nav.canPop(), matching every other bottom-nav-eligible screen: back
arrow + no bottom bar when entered from the drawer, no back arrow +
bottom bar when entered from the bottom nav.
https://claude.ai/code/session_01WZXxqCGYT4JEQBwwBNiUjm
NestZapButton had no ObserveZapIconState fallback (unlike NoteCompose's
ZapReaction), so after onPayViaIntent handed the invoice to an external
wallet the progress spinner stayed up indefinitely in the long-lived
NestActivity. Reset zappingProgress to 0 on every onPayViaIntent path.
The floating zap overlay was anchored TopEnd, colliding with the
hand-raise badge — and since the merge made zaps float from the
zapper's avatar (a likely hand-raiser), that overlap would be common.
Moved it to TopCenter, the only badge-free anchor.
Also dropped dead state (zapStartingTime / the non-animated
animatedProgress alias) and corrected stale comments left over from
the sender-grouping merge.
https://claude.ai/code/session_01EW11kUdiEYPuPti7vtD2AR
Adds technique-oriented Compose and Kotlin skills from chrisbanes/skills
to complement the existing codebase-oriented skills. Codebase skills cover
"where is X in Amethyst"; these cover "what is the correct Compose/Kotlin
design". Descriptions tagged as technique-layer and cross-links trimmed to
the vendored subset. CLAUDE.md skill table updated with a dedicated section.
https://claude.ai/code/session_01QE8CjoJXUt7RKtwGgzeMrb
Decompiled Coil 3.4.0's DiskLruCache to verify the assumption the wrapper
rests on. The original "inline inside commit()" framing was imprecise:
completeEdit() calls launchCleanup(), which launches on a limited-parallelism
IO scope — eviction is not on the commit thread. But the cleanup coroutine
runs trimToSize() *while holding the global DiskLruCache lock*, and
trimToSize -> removeEntry -> fileSystem.delete() does the unlink syscalls
under that lock. openSnapshot() (read) and openEditor() (write) both contend
on the same lock, so a cleanup pass blocks all feed-scroll reads/writes for
the duration of a burst of delete() syscalls. The wrapper still targets
exactly the right call; the mechanism is lock-holding, not thread-stealing.
- Correct the DeferredDeleteFileSystem KDoc to describe the verified
lock-holding mechanism.
- Add DeferredDeleteFileSystemCoilIntegrationTest: drives the real Coil 3
DiskCache over the wrapper, saturates it past maxSizeBytes, and asserts
eviction's unlinks land in the wrapper's queue (pendingCount > 0) with the
files still physically on disk until drained — the empirical complement to
the bytecode reading. A second test exercises the re-fetch-after-eviction
race against real Coil. Pins the assumption: a future Coil that stops
deleting via the injected FileSystem fails this test instead of silently
turning the wrapper into a no-op.
Adds RoomZapsStateTest mirroring RoomReactionsStateTest (grouping,
eviction, room-wide keying, idempotent snapshots, dedup). Also drops
the unused RoomZapsAggregator.isEmpty(), pins the zap chip's content
color to white since BitcoinOrange is theme-independent, and corrects
a few doc comments that overstated component/timer sharing.
https://claude.ai/code/session_01EW11kUdiEYPuPti7vtD2AR
onReactionEventGroupsByTargetAndEvictsOnTick asserted the pre-change
targetPubkey grouping (recentReactions.value[bob]); the aggregator
now groups by sourcePubkey so the chip rises from the reactor's
avatar. Renamed to ...GroupsBySender and assert under alice's key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reaction-side rework after extended testing on 2026-05-13:
* Promoted users weren't seeing speaker controls in their own
Amethyst — `StageControlsBar` gates on `isOnStage && ui.onStageNow`
and `ui.onStageNow` was stuck at false (never reset back to true
after some earlier path flipped it). Added a LaunchedEffect in
NestActivityContent that mirrors `isOnStageMe → ui.onStageNow`
whenever it becomes true, symmetric to the auto-stop effect.
* Reactions overlay sat inside AvatarAndBadges' wrap-content Box,
so adding/removing the chip shifted the inner Box's centre and
the role badges drifted. Lifted the overlay out to be a sibling
of AvatarAndBadges inside the outer fixed-size Box; badges now
stay anchored regardless of chip presence or animation state.
* Reaction grouping was by `targetPubkey` (NIP-25 p-tag) — emojis
showed on the speaker being reacted to, not the reactor. Switched
RoomReactionsAggregator to group by `sourcePubkey`. AudienceGrid
threads `reactionsByPubkey` so an audience reactor sees their own
emoji float from their audience-tab avatar too.
* Reaction chip animation: progress was an `Animatable.value` that
Compose wasn't refreshing in the layout-consuming layer (visible
bug: chip "blinked" with no movement). Replaced with a manual
`withFrameNanos` loop writing to a `MutableFloatState` read inside
a `graphicsLayer { … }` lambda — frame-clock animation that works.
Each kind-7 is its own chip keyed by event-id (no more
groupBy-content collapsing same-emoji bursts into one shared chip
that restarts on every arrival). Chips stack at a fixed-size 30 dp
Box with the emoji centred, so the X position is invariant under
glyph width. Multiple concurrent chips overlap at the right-bottom
corner in a `Box(BottomEnd)` (newest on top) rather than sliding
leftward in a `Row`.
* Bottom-drawer `RoomReactionPickerSheet` (hard-coded 6 emojis, no
NIP-30) replaced by a forked `RoomReactionPopup` that reuses
`ReactionChoicePopupContent` (same NIP-30 custom-emoji support,
same user-configured reaction set) but with two semantic deviations
for live audio rooms: empty `toRemove` so all buttons stay
"fresh", and the click handler signs+broadcasts a fresh kind-7
template directly instead of going through `Account.reactTo` —
which delegates to `ReactionAction.reactTo(note, …)` and
short-circuits on `note.hasReacted(by, reaction)`. The bypass
lets the user fire the same emoji repeatedly during a moment.
* Chip rendering matches NoteCompose: `RenderReactionContent`
handles `:shortcode:url` via `InLineIconRenderer` + `CustomEmoji`,
`"+"`→❤️, `"-"`→👎, anything else as Text.
Tests updated for sender-grouped aggregator semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit bundles several issues surfaced while testing host actions
inside an audio room.
NestActivity didn't mount `DisplayErrorMessages`, so every toast emitted
by nest code (promote, demote, kick, force-mute, errors) queued into a
StateFlow whose only collector lives in MainActivity. Mounted it inside
NestActivity.setContent so toasts now render in front of the room UI —
same way the leave-confirmation AlertDialog already does.
Host actions used to fire a synchronous "Promoted X" toast regardless of
whether `signAndComputeBroadcast` actually completed. Silent signer
failures (TimedOut, ManuallyUnauthorized, CouldNotPerform, etc. — all
Log.w-only in launchSigner) were invisible from the host's POV. Replaced
with a coroutine-bound failure toast that surfaces the exception class
+ message; success is implicit via the UI update.
The real cause of "promoted user stays in audience tab" turned out to be
stale presence: a kind-10312 emitted before the role grant (with
onstage=0) was pinning the freshly-promoted speaker to the audience tab.
buildParticipantGrid now takes a `roleGrantSec` parameter (the
kind-30312 created_at) and treats presence as authoritative only when
strictly newer. Pinned with two new tests covering both the
stale-ignore and fresh-respect cases. The leave-stage-on-another-client
flow keeps working because that emits a fresh onstage=0.
RoomParticipantActions.rebuild now uses
`(original.createdAt + 1L).coerceAtLeast(now())` so a same-second
promote→demote can't tie-break the wrong way under NIP-01's lowest-id
rule.
EditNestSheet's bottom row got squashed when the keyboard appeared —
the form fields couldn't shrink, so the Save/Cancel/Close row took the
hit. Split into a scrollable form column + sticky action row, wrapped
the outer column in imePadding.
SpeakerReactionOverlay was driving drift on a 100 ms `delay` loop over
the 10 s eviction window — produced 0.16 dp per tick (visibly stepped)
and the chip barely moved before disappearing. Replaced with
`Animatable.animateTo` on Compose's frame clock and a 6 s duration so
the chip pops, lingers visibly, then drifts up and fades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coil's DiskLruCache runs trimToSize() inline inside commit(): once the
image cache is saturated, every entry written during a feed scroll fires
a burst of synchronous delete() syscalls on the same thread, contending
with the reads that paint on-screen avatars — the scroll stall users see
go away after a storage wipe.
DeferredDeleteFileSystem is an okio ForwardingFileSystem handed to
DiskCache.Builder.fileSystem(). It intercepts delete() — exactly the call
eviction makes — enqueues the path, and unlinks it on a background
coroutine one path at a time with a yield() between each, keeping the IO
dispatcher responsive. Coil keeps full ownership of LRU order, keys, and
size accounting; only the syscall is rescheduled.
The re-create race (Coil re-fetching a URL whose entry was just evicted)
is handled by atomicMove/sink/appendingSink/openReadWrite/createDirectory
cancelling any pending delete of the path they are about to recreate. The
drainer unlinks each path under the same lock, so a concurrent write-path
call waits at most one in-flight unlink — O(1), never the O(N) burst.
This replaces the reverted KeyAccessLog/TrackingDiskCache/CoilDiskTrimmer
approach: no parallel LRU index, no persistent log, no extra heap — it
intercepts at the eviction syscall itself and leans on Coil's own LRU.
12 unit tests cover deferral, drain, dedup, the cancel-on-recreate paths
for every write entry point, pass-through of non-delete ops, and the
background drainer including a burst-then-recreate race.
This reverts commit b221dce8.
The KeyAccessLog/TrackingDiskCache/CoilDiskTrimmer approach rebuilt, persistently,
the LRU index Coil already keeps in memory (~50 MB heap for the key map), and the
trimmer drained the cache in one unbounded delete burst rather than spreading the
work — potentially a worse stall than the inline eviction it replaced, just less
frequent.
Replacing it with a FileSystem-layer delegate that defers the eviction unlink()
off the commit thread without any parallel bookkeeping.
F4A is AAC audio in an MP4 container (Adobe's variant of .m4a).
Register the extension in the media URL list so it routes to the
player, and map it to audio/mp4 so ExoPlayer picks the MP4 extractor.
https://claude.ai/code/session_01EuB46tfwBxtvRNDz9Ju99J
Subscribes to kind-9735 zap receipts tagged with the room's a-pointer,
feeds them into the nest chat ledger (so RenderChatZap surfaces them
the same way live streams do) and into a sliding-window aggregator
that drives a floating ⚡ chip over the targeted participant's avatar
— mirrors the existing reaction overlay's animation cadence so both
streams visually feel like one system.
The button itself wraps NoteCompose's zapClick / ZapAmountChoicePopup
/ ZapCustomDialog defaults against the room's AddressableNote, so the
amount-choice popup, custom-amount dialog and multi-payable routing
all behave like the standard note ⚡ button.
https://claude.ai/code/session_01EW11kUdiEYPuPti7vtD2AR
When Coil's DiskLruCache is saturated, every commit() triggers an inline
trimToSize() that performs file deletes synchronously on the IO dispatcher.
With Nostr feed scroll loading many large source avatars, each new write
evicts dozens of small entries, contending with concurrent reads of the
on-screen images and producing visible avatar-load latency and scroll jank.
Take ownership of eviction so size stays < maxSize during normal use:
- TrackingDiskCache wraps the real Coil DiskCache and records every key
access (openEditor/openSnapshot) into a persistent log.
- KeyAccessLog persists (timestamp, key) pairs in an append-only file under
filesDir, replayed on startup. Bounded at 200K entries with periodic
compaction.
- CoilDiskTrimmer runs on a 5-minute cadence and on onTrimMemory: when size
crosses 70% of maxSize it removes oldest keys via the public remove(key)
API until size drops to 55%. Coil's internal trim remains as a backstop
for keys we don't yet know about.
Unit tests cover persistence, idempotent load, corrupt-line recovery,
overflow handling, target/floor draining, oldest-first ordering, empty-log
fallback, and concurrent-call serialisation.
`AudioTrackPlayer` sized the AudioTrack ring at `max(minBuffer * 16,
250 ms)`. The intent (per the kdoc) was ~250 ms of jitter slack with
the device's `getMinBufferSize` as a floor — but the `* 16` multiplier
silently dominated on common devices. A Pixel reports `minBuffer ≈
40 ms` for 48 kHz mono; `× 16 = 640 ms`. Add the 200 ms preroll in
`NestPlayer` and decoded PCM sat in the ring for ~800 ms – 1 s before
the speaker played it.
Two-phone testing confirmed the symptom: the speaking-now ring lit up
~1 s before the listener's audio. The ring fires on
`onLevel(peakAmplitude(pcm))` in `NestPlayer.play()` immediately after
`decoder.decode()` succeeds — before `player.enqueue(pcm)` — so the
ring is real-time and the delay was entirely between `enqueue` and
the speaker. The web (NostrNests) listener uses an `AudioWorklet`
with no equivalent ring buffer, so its audio aligned with the wire.
Drop the `* 16` so the formula matches the intent: `max(minBuffer,
250 ms)`. On devices whose floor is below 250 ms (the common case)
the 250 ms target wins; on devices whose floor is above 250 ms (rare)
we still respect the device-reported minimum. Steady-state ear-to-
speaker delay drops from ~1 s to ~450 ms (250 ms ring + 200 ms preroll).
Removes the four trace points + debug-build auto-enable hook added in
the prior commit on this branch. They served their purpose locally
(confirmed the ~1s startup lag lives between pcm_decoded and
pcm_enqueued, i.e. AudioTrack ring backpressure), and don't need to
land in main.
Switch FavoriteAlgoFeedsListScreen from a plain Material Scaffold to
DisappearingScaffold and host an AppBottomBar bound to
Route.EditFavoriteAlgoFeeds, matching the pattern used by every other
tab-root feed (Badges, Articles, Bookmark Groups, ...). Hoist the
LazyColumn state so re-tapping the bar item scrolls to top.
When the user pins this screen as a bottom-bar entry, navigation lands
here via nav.navBottomBar() which marks the entry as a tab root, so
AppBottomBar renders and TopBarWithBackButton hides its back arrow.
When reached as a pushed screen from elsewhere, AppBottomBar's existing
canPop() guard hides the bar and the back arrow stays — preserving the
old behaviour for non-bottom-bar entry points.
The page indicator in the image/video zoom dialog sat too low on
Android devices, getting covered by the gesture bar or 3-button nav.
Apply navigationBarsPadding() to the indicator surface so it floats
above the system bar inset.
The Stepper introduced in this branch references MaterialSymbols.Remove
(U+E15B). Regenerated via tools/material-symbols-subset/subset.sh so the
shipped TTF actually contains the glyph.
The upstream NIP draft for kind:34551 community rules
(nostr-protocol/nips#2331) was renumbered from 9A to 9B per maintainer
feedback that slot 9a is already claimed by the push-notifications
draft (nostr-protocol/nips#2194):
https://github.com/nostr-protocol/nips/pull/2331#issuecomment-4442813289
This commit updates all human-readable references in the merged
community-rules code:
- 7 Kotlin files in quartz (CommunityRulesEvent, CommunityRulesValidator,
5 tag classes) — Kdoc comments
- 13 Kotlin files in amethyst (composer, feed filter, rules editor,
Account, AccountSettings, tests) — code comments + Kdoc
- 1 English strings file (values/strings.xml) — 2 user-facing strings
- 54 translation strings files (values-*-r*/strings.xml) — same two
strings, untranslated "NIP-9A" token replaced with "NIP-9B"
Kind number (34551), schema, tag names, and behaviour are unchanged.
No public API or DTO field renames. Pure docs/strings.
Verified: :quartz:spotlessCheck, :amethyst:spotlessCheck,
:commons:spotlessCheck all clean.
Adds four trace points on the listener pipeline so an end-to-end JSONL
capture (`adb logcat -s NestsTraceJsonl:D -v raw > nest-trace.jsonl`)
shows wall-clock latency at every stage:
frame_received (MoqLiteSession.drainOneGroup, after trySend)
frame_object_mapped (MoqLiteNestsListener.wrapSubscription map)
pcm_decoded (NestPlayer.play, after decoder.decode)
pcm_enqueued (NestPlayer.play, after player.enqueue)
Each event carries (sub_id, group_seq, ...) or (track_alias, obj_id, ...)
so the same frame can be followed through all four stages. Deltas
isolate which segment owns the observed startup-only ~1s lag:
subscribe_send → first frame_received : relay forward latency
frame_received → pcm_decoded : Opus decode wall-time
pcm_decoded → pcm_enqueued : AudioTrack ring backpressure
pcm_enqueued → next pcm_decoded : decode-loop scheduling
Tracing auto-enables on debug builds when `NestActivity` opens and
disables on `onDestroy` so release apps pay only the field-load
guard inside `NestsTrace.emit`. Capture instructions are inline at
the call site in `NestActivity.onCreate`.
- Wrap the when-branches in SelectableUserList, HiddenWordsList, and
MutedThreadsList in Box(modifier.fillMaxSize()) so the Scaffold
padding is applied to every state — Loading/Error/Empty/Loaded — not
just the LazyColumn. Fixes a regression where the loading spinner
and error UI rendered under the top bar.
- SettingsStepper now clamps `value` once into `[min, max]` and uses
the raw `value` (re-clamped) in the +/- handlers. If the model
starts below `min`, the first tap of `+` resyncs it to `min`
instead of jumping `min+1` (skipping a step). Display still falls
back to `unsetLabel` when `value <= 0`.
- WarnReportsTile no longer pre-coerces threshold to >= 1 at the call
site; the stepper handles it.
- EmptyState drops its now-unused modifier parameter.