Adds Czech, German, Brazilian Portuguese, and Swedish translations
for 19 new keys covering auto-create-drafts compose settings,
profile-UI section toggles, security-screen empty states and
section headers, external-resource comment scopes, and nest
host-action failure toasts.
Move the detectDragGestures pointerInput from the whole button card to
the six-dot drag handle icon so drag-and-drop reordering activates only
when the user grabs the handle.
https://claude.ai/code/session_01Jyu8dLYN7MXVNaVZE3u2M9
Members were sorted to float the current speaker to the top, so a
member jumped to the front when they started talking and slid back
~250ms after they stopped — a constant shuffle in any active room.
The speaking state is already shown in-place via the mic badge and
audio-level ring, so keep members in their stable arrival order.
https://claude.ai/code/session_01GvvNS1FRn6gGvHRC7dgEtP
Convert the per-account `disableClientTag` toggle into a positive
`addClientTag` setting that defaults to enabled, and relocate it from the
Security Filters screen to the Compose Settings page.
https://claude.ai/code/session_01TkmBzzEz3kKnrjpbrKCpsf
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
The bottom add-word text field sat behind the phone's navigation
and gesture bars. Apply the union of navigation bar and IME insets
to the bottom bar so it clears the system bars and rises with the
keyboard without a double-counted gap.
https://claude.ai/code/session_01KPkQbS9SYTigkkzYGU97Sz
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.