Commit Graph

13549 Commits

Author SHA1 Message Date
Claude a690777ef6 feat: show external-id scope as reply context for NIP-22 comments
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
2026-05-14 15:05:04 +00:00
Vitor Pamplona f7f4107ad7 Merge pull request #2886 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-14 10:00:28 -04:00
Crowdin Bot 1c59849fe4 New Crowdin translations by GitHub Action 2026-05-14 13:38:40 +00:00
Vitor Pamplona 56ca4214fe Merge pull request #2890 from vitorpamplona/claude/standardize-nav-behavior-cA6mg
Add bottom navigation bar to ScheduledPostsScreen
2026-05-14 09:36:56 -04:00
Claude 2a5e95f4cf fix: standardize ScheduledPosts nav-shell behavior
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
2026-05-14 13:26:47 +00:00
Vitor Pamplona fe9764072f Merge pull request #2887 from vitorpamplona/claude/add-zap-button-nest-KoZG4
feat(nests): add a zap button to the room action bar
2026-05-14 09:21:46 -04:00
Claude 4a3cea85cb fix(nests): clear zap spinner on wallet handoff; move overlay off the hand badge
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
2026-05-14 13:19:44 +00:00
Vitor Pamplona f19964da81 Merge pull request #2889 from vitorpamplona/claude/review-skills-library-86OMY
Add technique-layer Compose and Kotlin skills
2026-05-14 09:11:10 -04:00
Claude f45479615e Merge remote-tracking branch 'origin/main' into claude/add-zap-button-nest-KoZG4
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt
2026-05-14 13:08:13 +00:00
Claude f5354c2d34 docs: forbid inline fully-qualified class names in Kotlin
Adds a Kotlin style rule to CLAUDE.md requiring imports over
fully-qualified class names inline in function bodies.

https://claude.ai/code/session_01QE8CjoJXUt7RKtwGgzeMrb
2026-05-14 13:07:52 +00:00
Vitor Pamplona 82218bf73c Merge pull request #2888 from vitorpamplona/claude/fix-disk-space-usage-vBWWv
Defer image cache eviction unlinks to background thread
2026-05-14 09:03:06 -04:00
Claude 5177168583 docs(skills): vendor 10 Compose/Kotlin technique-layer skills
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
2026-05-14 12:59:46 +00:00
Claude 0e1d06cb06 test: verify Coil eviction routes through DeferredDeleteFileSystem; correct KDoc
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.
2026-05-14 12:51:51 +00:00
Claude 524bc90d5a test(nests): cover RoomZapsAggregator; tidy zap-overlay comments
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
2026-05-14 12:49:18 +00:00
Vitor Pamplona 0182439b55 fix NestViewModelTest for sender-grouped reaction aggregator
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>
2026-05-14 08:45:21 -04:00
Vitor Pamplona 78be534df5 nests room: stable reactions overlay + repeatable kind-7 + speaker controls
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>
2026-05-14 08:45:21 -04:00
Vitor Pamplona d21eb0e226 fix nests room host UX: visible toasts, promote propagation, edit-sheet keyboard, reaction animation
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>
2026-05-14 08:45:20 -04:00
Vitor Pamplona 82ce4df201 Merge pull request #2880 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-14 08:38:13 -04:00
Vitor Pamplona 38b5b677e8 Merge pull request #2885 from vitorpamplona/claude/add-audio-playback-f4a-FZy9V
feat: add audio playback support for .f4a files
2026-05-14 08:38:04 -04:00
Claude b6c6c0d2a2 perf: defer Coil disk-cache eviction unlink() off the write path
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.
2026-05-14 11:14:12 +00:00
Claude 472367b4ff Revert "perf: drain Coil disk cache off the write path to prevent scroll stalls"
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.
2026-05-14 11:05:13 +00:00
Claude 773d61692e feat: add audio playback support for .f4a files
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
2026-05-14 10:56:45 +00:00
Claude 6425fcd651 feat(nests): add zap button to the full-screen action bar
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
2026-05-14 04:43:25 +00:00
Claude b221dce87f perf: drain Coil disk cache off the write path to prevent scroll stalls
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.
2026-05-14 04:26:20 +00:00
Crowdin Bot 5b87356416 New Crowdin translations by GitHub Action 2026-05-14 03:31:05 +00:00
Vitor Pamplona fb068a13c6 Merge pull request #2883 from vitorpamplona/claude/trace-audio-pipeline-performance-EHJ45
fix(nests): cap AudioTrack ring at ~250 ms so audio tracks the speaking-now ring
2026-05-13 23:29:32 -04:00
Claude cd28de4eb1 fix(nests): cap AudioTrack ring at ~250 ms so audio tracks the speaking-now ring
`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).
2026-05-14 03:25:51 +00:00
Claude dec56c2ee9 Revert: remove NestsTrace listener-pipeline instrumentation
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.
2026-05-14 03:23:02 +00:00
Vitor Pamplona 17f207bb31 Merge pull request #2882 from vitorpamplona/claude/fix-dialog-paging-dots-yCTp5
fix: pad zoomable dialog pager dots above gesture/nav bar
2026-05-13 23:02:32 -04:00
Vitor Pamplona 3d8945128f Merge pull request #2881 from vitorpamplona/claude/add-bottom-nav-favorite-feed-l91GR
feat: show bottom nav on Favorite Algo Feeds screen
2026-05-13 23:01:58 -04:00
Claude 12650bd7fd feat: show bottom nav on Favorite Algo Feeds screen
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.
2026-05-14 02:53:02 +00:00
Claude d9ce949eda fix: pad zoomable dialog pager dots above gesture/nav bar
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.
2026-05-14 02:03:18 +00:00
Vitor Pamplona f3574081b5 Merge pull request #2879 from mstrofnone/docs/nip9b-renumber-comments
docs(community-rules): rename NIP-9A -> NIP-9B (upstream slot collision with #2194)
2026-05-13 21:48:02 -04:00
Vitor Pamplona 04743f700b Merge pull request #2878 from vitorpamplona/claude/modernize-security-filters-ui-IPUjW
Refactor SecurityFiltersScreen into modular settings screens
2026-05-13 19:57:57 -04:00
Claude a12d5d147d chore: regenerate MaterialSymbols font subset for Remove glyph
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.
2026-05-13 23:36:29 +00:00
m 2c3d006f05 docs: rename NIP-9A -> NIP-9B in code comments + user strings
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.
2026-05-14 09:29:59 +10:00
Claude 86c66920d3 chore(nests): instrument listener audio pipeline with NestsTrace
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`.
2026-05-13 23:09:31 +00:00
Claude 0b7399feb0 fix: pad Loading/Error states and tighten Stepper resync
- 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.
2026-05-13 23:09:06 +00:00
Claude 06e88cd30e refactor: address second-pass Security Filters audit
- Restore pull-to-refresh on BlockedUsers and SpammingUsers by wrapping
  SelectableUserList in RefresheableBox (regression from the first
  refactor; HiddenWords and MutedThreads remain non-pull-refresh to
  match the original behavior).
- Drop the redundant transientHiddenUsers subscription in
  InvalidateOnBlockListChange — hiddenUsers.flow already combines it.
  Also drop the meaningless accountViewModel key on LaunchedEffect.
- Move CountBadge and Stepper into SettingsSectionCard as
  SettingsCountBadge and SettingsStepper so other settings screens can
  reuse them. Widen the stepper value cell to min 40.dp so three-digit
  values like 999 fit comfortably.
- Drop the unused `enabled` flag from SettingsControlRow (sub-controls
  use SettingsSubControlRow for dimming). Tighten the docstring.
- Unify SwitchTile to take @StringRes Int params, matching the @StringRes
  annotations now applied to SettingsItem, SettingsSection, BlockListTopBar,
  SelectableUserList, EmptyState.
- Shorten property chains by binding `val security = …syncedSettings.security`
  per tile when more than one field is read; pass method references
  (accountViewModel::updateFilterSpam etc.) where possible.
- Hoist WarningType.entries to a local val so the segmented button row
  reads `count = options.size` instead of evaluating twice.
- MutedThreadsScreen: replace itemsIndexed with items (unused index) and
  use MaterialTheme.colorScheme.onPrimary instead of hardcoded Color.White
  for the "Unmute" button text.
- Add a @Preview for SecurityFiltersScreen, mirroring AllSettingsScreen.
2026-05-13 22:00:38 +00:00
Claude 6feca2cf17 refactor: address Security Filters audit findings
- Extract SettingsSection/SettingsItem/SettingsDivider/SettingsControlRow/
  SettingsBlockTile/SettingsSubControlRow into SettingsSectionCard.kt.
  AllSettingsScreen now consumes the shared components instead of carrying
  its own private copies, and SecurityFiltersScreen drops the parallel set
  it used to declare.
- BlockedContentRow folds into SettingsItem(trailing = { CountBadge(...) }),
  removing the bespoke navigation row.
- Replace the Spinner/Switch tile mix with WarnReportsTile = SwitchTile +
  SettingsSubControlRow(stepper), avoiding the nullable-icon code smell.
- Move the sensitive-content SegmentedButtonRow out of a cramped trailing
  slot and into SettingsBlockTile so it has full-width room.
- HiddenWordsScreen: replace the eager Column.forEach with LazyColumn, and
  add Modifier.imePadding() to the Scaffold so the keyboard no longer
  covers the Add-word field.
- Move SelectableUserList from BlockedUsersScreen.kt to SecurityListsCommon
  and drop the nullable onToggle (both call sites pass one).
- BlockListTopBar now takes selectedCount: Int instead of Set<*>; its
  UserFeedState branches are exhaustive (no else -> Unit fallthrough).
- CountBadge reserves min width so 0↔n transitions don't shift the row.
- Stepper relies on Material 3's enabled handling instead of computing
  alpha by hand and tinting LocalContentColor in three places.
- Distinct icons per tile/category (Visibility, Code, VisibilityOff, etc.)
  so the same MaterialSymbols.Tag isn't reused for unrelated rows.
2026-05-13 21:34:53 +00:00
Claude 2816ceb479 feat: split Security Filters into a hub with per-category screens
Replaces the cramped single-screen layout (header settings + four tabs)
with a hub that uses the same grouped-card pattern as AllSettingsScreen,
plus a dedicated screen per blocked-content category. The tile control
mix is also modernized:

- "Show sensitive content" is now a 3-way SegmentedButton instead of a
  free-floating spinner sitting awkwardly next to switches.
- Numeric prefs (report threshold, max hashtags) use a stepper (- / + )
  instead of free OutlinedTextFields with no commit affordance.
- The report-threshold tile renders inline under its toggle and dims
  when the toggle is off, instead of appearing/disappearing and shifting
  layout.
- Each tile is a row with leading icon, title, description, trailing
  control - so the spinner/switch/stepper visual rhythm stays consistent.

The hub's "Blocked content" section shows a count badge per category and
routes to its own screen (Routes.BlockedUsers, SpammingUsers, HiddenWords,
MutedThreads). The hidden-words screen now docks the "Add word" field as
a bottom bar so it doesn't compete with the list for vertical space, and
selection mode is scoped per-screen instead of shared across tabs.
2026-05-13 20:48:53 +00:00
Vitor Pamplona dbd7266d8f better description of the draft option 2026-05-13 16:29:17 -04:00
Vitor Pamplona 8dd4ed4c6d Merge pull request #2877 from vitorpamplona/claude/add-profile-settings-page-ezkgF
Add profile UI settings to customize visible sections and feeds
2026-05-13 15:56:26 -04:00
Claude 128048f043 perf: stop recomposing RenderScreen on every profile-tab swipe
viewedTab was a MutableState read inside RenderScreen at
visibleTabs.indexOf(viewedTab), so every pager swipe — which writes to
viewedTab from snapshotFlow — invalidated the enclosing restart group
even though rememberPagerState's initialPage is never re-applied after
the first composition.

Move the tracker to a plain holder class (ViewedTabRef). Reads inside
the key(visibleTabs) block no longer subscribe to snapshot changes, so
swipes only recompose the tab row / pager content as intended.
2026-05-13 19:50:52 +00:00
Claude 251699beaa fix: honor Profile UI toggles in subscriptions and pager state
- Gate WatchLifecycleAndUpdateModel(appRecommendations) on the
  showProfileAppRecommendations toggle so the viewmodel no longer
  refreshes when the section is hidden.
- Thread loadFollowers / loadZapsReceived through UserProfileQueryState
  and the matching sub-assemblers. When the user hides those tabs,
  UserProfileFilterAssembler stops emitting their relay filters and
  the live REQs drop.
- Re-pin the profile pager to the tab the user was viewing whenever
  the visible-tabs list changes, using key(visibleTabs) +
  rememberPagerState(initialPage = ...) and a snapshotFlow that tracks
  the currently-viewed ProfileTab. Prevents the pager from silently
  shifting to a different tab when one is toggled off in settings.
2026-05-13 19:50:52 +00:00
Claude f26f3626f1 feat: add Profile UI settings page
New "Profile UI" settings page lets users toggle which profile-screen
sections are loaded and displayed. All toggles default to on.

Toggles:
- Profile Badges (NIP-58 badges row)
- App Recommendations (apps row in profile header)
- Zap Received Feed (received zaps tab)
- Followers Feed (followers tab)

Tabs are filtered from the profile pager so disabled feeds no longer
allocate a page or fire their subscriptions.
2026-05-13 19:50:52 +00:00
Vitor Pamplona 6b06db9395 Merge pull request #2876 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 15:49:21 -04:00
Crowdin Bot 216ce24839 New Crowdin translations by GitHub Action 2026-05-13 19:45:26 +00:00
Vitor Pamplona 680aacf5b9 fix nests demoted speakers stuck on stage and broadcasting
Three related bugs when a host removes a speaker from the stage on
nostrnests by editing the kind-30312 to drop their p-tag:

1. NestRoomFilterAssembler didn't subscribe to the kind-30312 itself
   while in-room — only chat/presence/reactions (#a) and admin
   commands. Once joined, the room event was frozen on whatever
   version loaded the screen, so demotions never propagated. Added
   a per-relay filter on `kinds=[30312], authors=[host], #d=[dTag]`.

2. Even if the demotion did propagate, `BroadcastHandle` kept
   running. Only the manual Leave Stage button called
   `stopBroadcast()`. Added a LaunchedEffect that tears down the
   broadcast when the local user falls off `participantGrid.onStage`
   while still publishing — covers both demote-by-host and
   leave-stage-on-another-client.

3. The auto-stop reliably exercised a pre-existing
   NestForegroundService bug: when `startListening` was called to
   demote from mic+media to media-only, `intent.action == null`
   fell through to `else -> promoted`, keeping mic=true. The service
   then asked startForeground for FOREGROUND_SERVICE_TYPE_MICROPHONE
   after the mic had been released, threw SecurityException on
   Android 14+, runCatching swallowed it, stopSelf ran without
   startForeground → ForegroundServiceDidNotStartInTimeException.
   The explicit-demote branch now returns false; only intent==null
   (OS sticky-restart) preserves the prior promoted state.
2026-05-13 15:41:51 -04:00
Vitor Pamplona 20a2270434 fix nests room author shown in audience with no audio
nostrnests publishes kind-30312 without a self-`p`-tag for the room
author — they're the implicit host. The lobby card already handles
this (NestJoinCard) but the in-room code did not: the author fell
into the pure-audience branch with role=null AND was missing from
the audio subscription set, so the host appeared as a regular
listener and no sound played.

buildParticipantGrid now takes hostPubkey and synthesizes a virtual
`["p", host, "", "host"]` when absent, so the existing presence-aware
onstage/audience rules apply uniformly — including "host leaves
stage" (onstage=0 → drops to audience, role stays HOST).

NestActivityContent owns the grid now and derives onStageKeys from
it, so the StageGrid render and the MoQ subscription set stay in
lockstep when anyone (host or speaker) steps off stage.
2026-05-13 15:41:51 -04:00