Commit Graph

12644 Commits

Author SHA1 Message Date
Claude 0fafd34537 fix(nests): emit canonical title tag and keep mute badge visible while muted
Two interrelated bugs from the deployed-schema flip:

1. Rooms created in Amethyst didn't appear in nostrnests's "Live Now"
   lobby. The lobby filter in NestsUI's skeleton.js requires a `title`
   tag on every kind:30312 — events without one are dropped from
   `live`/`planned`/`ended`. RoomNameTag still emitted the legacy
   `room` name, so our rooms passed our own readers (which tolerate
   both names) but were invisible to NestsUI. Flip canonical to
   `title`, keep `room` as legacy on read.

2. The mute badge over the avatar disappeared shortly after toggling
   mute, even though the broadcaster was still muted. Two causes:

   - ParticipantsGrid gated both the muted ring and the MicStateBadge
     on `member.publishing`. But per the deployed kind-10312 semantics
     `publishing=0` whenever `muted=1` (NestRoomPresencePublisher
     mirrors this — `publishingNow = broadcasting && !isMuted`), so
     the badge that was supposed to *indicate* mute was hidden
     exactly when it became relevant. Show the badge when on stage
     and either publishing OR muted; gate the muted ring on `muted`
     alone.

   - The presence heartbeat captured `micMutedTag` / `publishingTag`
     / `onstageTag` / `handRaised` at LaunchedEffect launch and never
     refreshed them. The 500 ms debounced LaunchedEffect did publish
     the new mute state, but ~30 s later the heartbeat overwrote it
     with the captured pre-toggle values, flipping `publishing`
     back to 1 and `muted` back to 0 server-side. Wrap the dynamic
     fields in `rememberUpdatedState` so the heartbeat reads the
     latest snapshot on every refresh; keep the existing key set
     (event/handRaised/onstage) for prompt re-emission on those
     transitions.

Tests updated for the new canonical `title` emission; both jvmTest
suites pass.
2026-04-29 20:45:32 +00:00
Claude 86537edef5 refactor(quartz): introduce ObservableEventStore so projections see ephemerals
Splits "events accepted for persistence" from "events accepted for
observation". The new ObservableEventStore wraps any IEventStore and
owns events: SharedFlow<Event>:

- Non-ephemeral events forward to the inner store; success → emit, any
  rejection (expired, NIP-09 / NIP-62 tombstone, NIP-01 supersession
  loser) → no emit.
- Ephemeral events (kinds 20000-29999) skip persistence entirely but
  still emit, so an open EventStoreProjection renders them while alive.
  They vanish from any future seed because the DB never had them.

Reverts the previous round of plumbing inside SQLiteEventStore /
FsEventStore: stores no longer carry an inserts SharedFlow. IEventStore
goes back to a clean read/write contract. ObservableEventStore is the
single place that decides what makes it onto the projection bus.

EventStore (the SQLite convenience class) embeds an ObservableEventStore
internally so existing call sites like `store.observe(filter, scope)`
keep working without changes.

EventStoreProjection now collects via Flow.onSubscription so the
collector subscription is established before the seed query runs —
fixes a race where an insert immediately after `ready.await()` could
land in the SharedFlow before the collector was subscribed.

Tests:
- ephemeralEventsAppearInProjection — kind-22000 event reaches items
  but isn't queryable from the inner store, and a fresh projection
  on the same store gets an empty seed.
- 14/14 projection tests + 219/219 other store tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-29 20:34:56 +00:00
Claude c54f34ef08 refactor(quartz): make EventStoreProjection store-agnostic, NIP-aware
The projection now sits on top of any IEventStore, not just SQLite.
Stores publish a simple `inserts: SharedFlow<Event>` (one event per
successful insert), and the projection itself replays the NIP rules
against that stream:

- NIP-01 supersession with the lexical-id tiebreaker, for both
  replaceables (kind 0/3/10000-19999) and addressables (30000-39999).
  Both now share a single in-place update path.
- NIP-09 deletions, with the original-author check (cross-author
  kind-5s are inert, matching the store).
- NIP-62 right-to-vanish, scoped by the projection's relay arg.
- NIP-40 expiration via a per-projection ticker that drops slots whose
  expiration tag has lapsed.

Out-of-band store mutations (`delete(id)`, `clearDB()`, the periodic
`deleteExpiredEvents()` sweep) are no longer visible to projections —
they're maintenance ops; projections re-seed when their scope is
restarted.

Removed from SQLiteEventStore:
- ChangeLogModule + temp-table + AFTER DELETE trigger.
- StoreChange sealed type and the per-mutation drain/publish path.

Both SQLiteEventStore and FsEventStore now satisfy
`IEventStore.inserts`. FsEventStore.insertLocked returns Boolean so
no-op idempotent retries (canonical already on disk) don't re-publish.

Tests:
- EventStoreProjectionTest moves to `store/projection/`, gains four
  new cases: NIP-01 out-of-order rejection, NIP-09 cross-author
  inertness, NIP-62 cross-author no-op, NIP-40 ticker-driven removal.
- 13/13 projection tests + 232/232 total store tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-29 19:56:00 +00:00
Claude aadb347b84 feat(nests): adopt deployed nostrnests schema (streaming/auth/live + paired 10112)
The deployed nostrnests reference (NestsUI v2 + moq-auth + moq-rs)
emits a different on-the-wire schema than the previous EGG-01 / EGG-09
drafts and Quartz writers. Verified by reading the production
NestsUI bundle. The deployed schema is now canonical:

kind:30312 (room event)
  - relay URL → ["streaming", url]   (was ["endpoint", url])
  - auth URL  → ["auth",      url]   (was ["service",  url])
  - live status → ["status", "live"]   (was "open")
  - ended status → ["status", "ended"] (was "closed")
  - room name → ["title", name]        (was ["room", name])

kind:10112 (user MoQ-server list)
  - 3-element entries: ["server", relay, auth]
  - first-element name "relay" accepted as a synonym (legacy)
  - 2-element entries: derive auth host by replacing leading "moq."
    with "moq-auth.", or prepending "moq-auth." otherwise

Implementation
- ServiceUrlTag.TAG_NAME = "auth", LEGACY_TAG_NAME = "service"
- EndpointUrlTag.TAG_NAME = "streaming", LEGACY_TAG_NAME = "endpoint"
- StatusTag enum: PLANNED/LIVE/PRIVATE/ENDED with code "live"/"ended";
  legacy "open"/"closed" still parsed on read
- NestsServersEvent: emit/read 3-element [server, relay, auth] tags
  with the legacy-shape tolerances above; expose a NestsServer pair
- Account.nestsServers.flow now produces List<NestsServer> pairs
- NestsServersScreen: rewritten edit-field asks for both URLs,
  recommended row shows the nostrnests pair, list rows show both URLs
- NestsScreen first-time setup writes the nostrnests pair, not a
  single URL; gate the create FAB on both URLs being parseable
- CreateNestViewModel: drop the resolveServerPair hardcoded mapping —
  the saved pair is now authoritative; defaults stay correct for an
  empty kind-10112 list

Specs
- EGG-01 rewritten to canonical streaming/auth/live/ended with a
  legacy-spelling table; example uses the real nostrnests pair
- EGG-02 references "auth" tag throughout; error taxonomy says "ended"
- EGG-09 rewritten to 3-element server tag with derivation fallback
- New nestsClient/specs/nip-53-proposed.md — a single self-contained
  proposed update to upstream NIP-53 covering kind 30312 + kind 10112
  with the deployed schema and the JWT-mint flow

All existing unit tests adjusted; quartz:jvmTest, amethyst:testPlayDebugUnitTest,
and nestsClient:jvmTest pass.
2026-04-29 19:32:53 +00:00
Claude d370784f3f feat(quartz): reactive EventStoreProjection over SQLiteEventStore
Adds a stateful observer that turns a Filter into a StateFlow<List<MutableStateFlow<Event>>>:

- A TEMP table + AFTER DELETE trigger on event_headers logs OLD.id for every row
  that leaves the store, regardless of cause (replaceable / addressable
  supersession, NIP-09 deletion, NIP-62 vanish, NIP-40 expiration sweep, manual
  delete, clearDB).
- SQLiteEventStore drains the log around each writer unit of work and emits a
  StoreChange(inserted, removedIds) on a SharedFlow.
- EventStoreProjection seeds itself from the store, then maintains stable
  MutableStateFlow handles keyed by (kind:pubkey:dtag) for addressables and by
  id otherwise. Addressable updates mutate the handle's value in place — list
  reference stable; insert / remove rebuilds the list reference.
- 11 new tests cover seed, insert, replaceable update, addressable update,
  NIP-09 deletion, NIP-62 vanish, NIP-40 expiration, manual delete, limit
  enforcement, non-matching insert, and close-cancels-listener.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-29 19:02:40 +00:00
Claude 63555db48a fix(nests): use moq-auth.nostrnests.com for JWT mint
The previous defaults pointed both `service` and `endpoint` URLs at
`moq.nostrnests.com:4443`. That host only listens on UDP/QUIC for the
WebTransport relay — TCP :4443 is unreachable. OkHttp can't speak HTTP/3,
so the JWT-mint POST hangs and fails with `ConnectException`, leaving
the room screen stuck on "Reconnecting".

The real nostrnests deployment co-locates auth and relay on different
hosts (verified against the production NestsUI bundle):

    relay (WebTransport, QUIC only):  https://moq.nostrnests.com:4443
    auth  (JWT mint, regular HTTPS):  https://moq-auth.nostrnests.com

Split the defaults accordingly and add a `resolveServerPair` helper that
maps a single saved kind-10112 URL onto the (service, endpoint) pair the
kind-30312 event needs. The helper recognises both the auth-host and
the legacy relay-host the prior defaults wrote, so users upgrading from
the broken version migrate transparently the next time they open the
create-room sheet. Community deployments that genuinely co-locate keep
working via the fallback.

Update the recommended-servers list and the kind-10112 documentation to
match: the stored `server` URL is the moq-auth base (the URL that ends
up in the kind-30312 `service` tag).
2026-04-29 18:20:21 +00:00
Vitor Pamplona 47f700afb3 Merge pull request #2644 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-29 08:14:11 -04:00
Crowdin Bot 893ac76e3f New Crowdin translations by GitHub Action 2026-04-29 12:11:59 +00:00
Vitor Pamplona 94d3982f88 Merge pull request #2645 from greenart7c3/claude/add-payment-targets-settings-QgGFg
Add payment targets navigation to settings screen
2026-04-29 08:10:12 -04:00
Claude 8078e72bc1 feat: expose payment targets settings in account settings
Add a navigation row in AllSettingsScreen to open the existing
PaymentTargetsScreen, so users can configure their NIP-A3 payment
targets (kind 10133) from the account settings menu.
2026-04-29 11:59:39 +00:00
Vitor Pamplona aef0683bd6 Merge pull request #2639 from davotoula/fix-sonar-empty-functions
Fix sonar empty functions and string literals
2026-04-29 07:53:22 -04:00
Vitor Pamplona fe02f61d46 Merge pull request #2642 from mstrofnone/fix/last-seen-formatting
Fix Last Seen formatting for older timestamps ("Last seen Jan 14 ago" → "Last seen on Jan 14, 2026 (3 months ago)")
2026-04-29 07:53:07 -04:00
Vitor Pamplona da30e7ada0 Merge pull request #2643 from mstrofnone/fix/vlc-download-retry
ci(desktop): retry vlcDownload on transient network failures
2026-04-29 07:51:24 -04:00
David Kaspar 19e19192e3 Merge pull request #2641 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-29 12:32:55 +02:00
mstrofnone fd7ce48964 ci(desktop): retry vlcDownload on transient network failures
Every desktop packaging job (Windows MSI, macOS DMG, Linux DEB) currently
breaks on the smallest blip when fetching VLC from get.videolan.org. The
`ir.mahozad.vlc-setup` plugin registers `vlcDownload` / `upxDownload`
tasks that extend `de.undercouch.gradle.tasks.download.Download`, but it
does not configure retries or a generous read timeout, so a single
`SocketTimeoutException` aborts the build.

Recent main runs all failed at the same step:

    > Task :desktopApp:vlcDownload FAILED
    > A failure occurred while executing ...DefaultWorkAction
       > java.net.SocketTimeoutException: Read timed out

Configure all `Download` tasks in this project with:

  - 5 attempts total (initial + 4 retries),
  - 30 s connect timeout,
  - 5 min read timeout (VLC archives are 40-90 MB and the mirror can be
    slow under load),
  - `tempAndMove` so a partial download from one attempt cannot poison
    the next.

This is a CI-only change \u2014 it does not alter what gets bundled, just
makes the fetch resilient.
2026-04-29 20:21:18 +10:00
M 1df6504aac Fix Last Seen formatting for older timestamps
The profile 'Last seen' line was rendered by feeding timeAgo() output
into the 'Last seen %1$s ago' template. timeAgo() returns a bare
absolute date (e.g. 'Jan 14' or 'Jul 27, 2024') for anything older
than a month, so the result was nonsensical:

  - 'Last seen Jan 14 ago'
  - 'Last seen Jul 27, 2024 ago'

Replace the call with a new lastSeenSentence() helper that always
returns a self-contained, grammatical sentence:

  - 'Last seen 5 minutes ago'
  - 'Last seen 2 hours ago'
  - 'Last seen 3 days ago'
  - 'Last seen 2 weeks ago'
  - 'Last seen on Jul 27, 2024 (9 months ago)'
  - 'Last seen on Jan 14, 2024 (1 year ago)'

Anything older than a week now also shows the absolute date alongside
a coarse relative duration, so users can see exactly when the activity
happened without giving up the human-readable 'X ago' framing.

Adds plural-aware duration_minutes/hours/days/weeks/months/years
resources plus last_seen_on_date / last_seen_just_now / last_seen_never
helper strings. The existing 'last_seen' string keeps its placeholder
shape so existing translations continue to work for the recent-duration
case.
2026-04-29 19:11:09 +10:00
Crowdin Bot 5d314a3939 New Crowdin translations by GitHub Action 2026-04-29 08:42:00 +00:00
David Kaspar f581574173 Merge branch 'main' into fix-sonar-empty-functions 2026-04-29 10:41:43 +02:00
David Kaspar 6dd5658947 Merge pull request #2640 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-29 10:40:29 +02:00
Crowdin Bot a6d5640206 New Crowdin translations by GitHub Action 2026-04-29 08:17:06 +00:00
davotoula 3ebf6cf02a l10n: translate Nests audio-rooms, AI alt-text, and tracked broadcasts strings
Adds 124 missing translations across cs-rCZ, pt-rBR, sv-rSE, and de-rDE
covering the new Nests (NIP-53 audio-rooms) UI, AI-suggested alt-text
hints, tracked broadcasts setting, and meeting/live-stream tags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:14:54 +02:00
davotoula fdcd83e0c6 extract duplicated string literals into named constants 2026-04-29 09:59:27 +02:00
David Kaspar 708603d13b Merge pull request #2636 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-29 09:56:54 +02:00
davotoula 3332d8b530 add no-op comments to empty methods 2026-04-29 08:55:36 +02:00
Crowdin Bot b18b645233 New Crowdin translations by GitHub Action 2026-04-29 03:56:15 +00:00
Vitor Pamplona 250378b657 Merge pull request #2638 from vitorpamplona/claude/fix-listener-test-timeout-HqseI
test(nests): fix flaky ReconnectingNestsListenerTest on macOS
2026-04-28 23:54:25 -04:00
Claude 099da6e7b0 test(nests): fix flaky ReconnectingNestsListenerTest on macOS
`subscribeSpeaker_survives_session_swap` and
`unsubscribe_before_session_swap_releases_handle` had two race
conditions exposed only on macOS scheduling:

1. ScriptedListener.subscribeCount is incremented inside
   opener(listener) — BEFORE the wrapper's pump reaches
   `handle.objects.collect { frames.emit(it) }`. Waiting on
   subscribeCount alone doesn't prove the pump is collecting
   from first.frames. An emit upstream before the pump's collect
   registers is silently dropped.

2. The wrapper's outer `frames` SharedFlow is replay=0. The
   `scope.async { take(2).toList() }` consumer might not have
   subscribed by the time the test thread races into the first
   emit, dropping the value.

Both are fixed by waiting for actual subscription registration:
- first/second.frames.subscriptionCount.first { it > 0 } — flips
  to 1 only after the pump's collect lands, which is strictly
  after liveHandleRef.set(handle).
- onSubscription + CompletableDeferred — fires after the
  consumer's collector is registered against the SharedFlow.
2026-04-29 03:16:23 +00:00
Vitor Pamplona 6c8c5eb7a5 Merge pull request #2637 from vitorpamplona/claude/fix-windows-jvm-test-JA55M
fix(quartz): skip schnorr256k1 benchmark when native lib fails to load on Windows
2026-04-28 21:45:13 -04:00
Claude 21796cc4ad fix(quartz): skip schnorr256k1 benchmark when native lib fails to load on Windows
The triple benchmark probed libschnorr256k1 with a try/catch that only
handled UnsatisfiedLinkError. On Windows the loader raises
IllegalStateException when the JNI .dll for the platform isn't packaged,
which propagated out as a test failure instead of skipping the C row.

Mirror the broader catch already used in Secp256k1CrossValidationTest so
the benchmark prints a SKIP-style note and continues with the ACINQ +
Kotlin rows on platforms where the C lib isn't available.
2026-04-29 01:28:27 +00:00
Vitor Pamplona ee7eada00e Fixes top bar filter for Nests 2026-04-28 20:38:34 -04:00
Vitor Pamplona 86b1b02211 Last test to fix 2026-04-28 20:23:13 -04:00
Vitor Pamplona fd1b9f44b4 Merge pull request #2635 from vitorpamplona/claude/review-nostrnests-filtering-3EoIe
Add presence-based freshness filtering for Nests feed
2026-04-28 20:17:10 -04:00
Vitor Pamplona 29cf589b37 Merge pull request #2634 from vitorpamplona/claude/fix-keyboard-textfield-overlap-BA65q
Fix IME padding and window insets in NestFullScreen
2026-04-28 20:13:47 -04:00
Claude 5eea6ec8e6 fix(nests): keep chat textfield visible above keyboard on NestFullScreen
Activity used the system default soft-input behavior (pan), so opening
the keyboard slid the whole screen up, partially hiding the composer
and putting the top of the screen out of reach. Set adjustResize on
NestActivity and add consumeWindowInsets + imePadding to the Scaffold
body so the column reflows above the IME and the textfield stays in
view.
2026-04-28 22:51:07 +00:00
Claude 0c875b150b feat(nests): tighten lobby filter to mirror NostrNests
Bring the audio-room lobby filter closer to the NostrNests reference:

- Reject service/endpoint URLs that aren't HTTPS, dropping legacy
  `wss+livekit://` rooms a moq-lite client can't reach.
- Drop PLANNED rooms whose `starts` is more than 1 h in the past or
  more than 30 d in the future.
- Add a single lobby-wide kind-10312 REQ (10 min, limit 500) per
  active relay and use it to hide OPEN/PRIVATE rooms whose host
  crashed without flipping status to CLOSED. Brand-new rooms keep
  showing for the freshness window so they don't pop in late.
- Expand follow-style top filters to include rooms whose p-tagged
  speakers are followed, not just the host.
2026-04-28 22:45:18 +00:00
Vitor Pamplona 2723c48f88 Better design for audiences 2026-04-28 18:20:38 -04:00
Vitor Pamplona 12ac22b53b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-28 18:08:51 -04:00
Vitor Pamplona a538138b25 Merge pull request #2633 from vitorpamplona/claude/fix-amethyst-nest-reconnecting-T8DSP
Update MOQ relay URLs to include port 4443
2026-04-28 18:04:36 -04:00
Claude 882a7b5501 Revert "fix(net): prefer IPv4 over IPv6 for OkHttp + QUIC dial"
This reverts commit 23fab05c8a.
2026-04-28 21:56:30 +00:00
Vitor Pamplona 356ddffa99 Merge pull request #2630 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-28 17:55:35 -04:00
Vitor Pamplona de773d06f9 Fixes test cases 2026-04-28 17:54:29 -04:00
Crowdin Bot 3fd30cc820 New Crowdin translations by GitHub Action 2026-04-28 21:54:06 +00:00
Vitor Pamplona b81bebf6bb Merge pull request #2632 from vitorpamplona/claude/fix-gesture-nav-spacing-BC4nv
Fix window insets handling in NestActionBar
2026-04-28 17:52:24 -04:00
Claude 045bc36371 fix(nests): respect navigation bar insets in NestActionBar
The bottom action bar in NestFullScreen was rendering under Android's
gesture navigation area in edge-to-edge mode, leaving the leave / talk
/ react buttons partially obscured. Apply BottomAppBarDefaults
windowInsets to the surface so it pads above the system navigation.
2026-04-28 20:59:22 +00:00
Vitor Pamplona dcc6a7e07d Merge pull request #2631 from vitorpamplona/claude/wire-jni-secp256k1-kYhKZ
Update schnorr256k1-kmp dependency to v1.0.3 and fix group ID
2026-04-28 16:28:52 -04:00
Claude 23fab05c8a fix(net): prefer IPv4 over IPv6 for OkHttp + QUIC dial
The Android emulator (and a number of dual-stack networks where the
IPv6 path is broken) advertises working v6 connectivity but silently
drops outbound packets. With the JDK / RFC 6724 default of AAAA-first,
any host that publishes a stale or unreachable AAAA record — e.g.
moq.nostrnests.com, whose Linode v6 currently doesn't accept
connections — causes every HTTPS call and every QUIC handshake to
fast-fail with ConnectException, parking the Nests room screen on
"Reconnecting" indefinitely.

Plug a small Dns wrapper into both OkHttp factories that puts
Inet4Address entries ahead of v6 in the resolved list (v6 stays as
fallback for genuinely v6-only hosts), and switch UdpSocket.connect
from getByName to getAllByName + IPv4-first selection so the QUIC
audio path makes the same choice as the HTTPS auth POST.
2026-04-28 20:26:49 +00:00
Claude ecb985e2b4 build(quartz): bump schnorr256k1-kmp to 1.0.3 with bundled JVM JNI
Pulls in the per-platform JNI shards (linux-x86_64, linux-aarch64,
darwin-x86_64, darwin-aarch64) that the upstream library now publishes as
runtime dependencies of schnorr256k1-kmp-jvm. The 1.0.3 release also
moved the groupId to com.vitorpamplona.schnorr256k1.

Net effect: Secp256k1CrossValidationTest's customCImplementationMatchesAcinqAndKotlin
no longer prints SKIP, and the Custom C(JNI) column in
Secp256k1TripleBenchmark now reports numbers (verifySchnorr ~25.6k ops/s,
signSchnorr cached pk ~57.4k ops/s) instead of (N/A). 188/188 tests pass
in the secp256k1 suite on a stock JVM with no LD_LIBRARY_PATH or
-Djava.library.path needed.

https://claude.ai/code/session_01XEfLBStDum7h8Brwo7qFXt
2026-04-28 20:20:28 +00:00
Vitor Pamplona 17e04fc692 Centralizes the empty mesaage. 2026-04-28 16:18:24 -04:00
Claude 412b2a285c fix(nests): point default moq.nostrnests.com URLs at port 4443
The public nostrnests deployment serves moq-auth + moq-relay on
:4443; :443 is not the live endpoint. With the old defaults a
freshly-created room could never mint a JWT, so the room screen
parked on "Reconnecting" forever and the room never showed up in
the nostrnests UI.
2026-04-28 20:10:35 +00:00
Vitor Pamplona 629915dc6c Merge pull request #2628 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-28 15:58:45 -04:00