Commit Graph

12673 Commits

Author SHA1 Message Date
davotoula 212fdf739c updated translations cz,se,de,pt 2026-04-30 09:54:46 +02:00
Claude 37bbb45090 fix(bottombar): preload feeds for user-pinned icons, not the default 5
LoggedInPage hardcoded the Home/Messages/Video/Discover relay subscriptions,
so users who customised the bottom bar still paid bandwidth for the four
defaults while the icons they actually pinned never preloaded. Drive the
preloaders from uiSettingsFlow.bottomBarItems so subscriptions track the
chosen list reactively.
2026-04-30 03:47:58 +00:00
Claude bfd3ddaca6 feat(home): user-configurable home tabs (new threads, conversations, everything)
Adds a Home Tabs settings screen letting users pick which tabs appear on Home:
New Threads, Conversations, and a new combined Everything tab. The tab row is
hidden whenever only one tab is active, so users can keep a single feed view.
At least one tab always remains active.
2026-04-30 03:29:41 +00:00
Claude 6da9af2a81 feat(nests): use mic icons for talk / stop talking
The text Talk / Stop Talking buttons made it easy to misread the
broadcast state. Swap them for large filled mic-icon buttons so the
mic state is unmistakable: MicOff in primary color when idle (tap to
go live), Mic in error color when broadcasting (mic is open, tap to
stop).
2026-04-30 03:28:53 +00:00
Claude ae6343cd73 refactor(quartz): simplify EventStoreProjection (-50 lines, -1 bug)
Review of the projection found one real bug and four redundancies:

Bug fix: handleInsert tracked `retained` inline as it walked filters,
flipping it to false when the slot self-evicted from a filter's cap.
But if filter A retained the slot (cap=null or with room) and filter B
later cap-evicted it, retained ended up false even though the slot was
still in A's set — leaking a slot into a per-filter set with no
byId/byAddress entry. Fix: compute retention after the loop with
`perFilter.values.any { it.contains(slot) }`.

Simplifications:
- Drop the redundant `ordered: SortedSet<Slot>` field. It was only
  read by publish(); it's the deduped sorted union of perFilter
  values. Compute it lazily at publish time instead. Removes the
  field, all the ordered.add/ordered.remove calls, and one helper.
- Collapse `dropSlotIndexes` and `removeSlot` paths into
  `removeIndexes` (id+address) + `removeSlot` (also clears per-filter
  sets). The eviction loop calls removeIndexes; everywhere else
  calls removeSlot.
- Drop the defensive `byAddress[address] === slot` identity check —
  with byId as the primary index, removeSlot is only called once per
  slot, and rekey only ever moves byId, never byAddress.
- Pull the iterate-and-drop pattern into a single `dropWhere(predicate)`
  inline helper. handleVanish, applyDeleteByFilter, and
  applyDeleteExpired all collapse to one-liners.
- Apply now returns Boolean from each branch and publish() runs once
  at the end, instead of three separate `if-publish` blocks.

Net: 233 → 142 lines. Same behaviour, plus the retained bug fix.
17/17 projection tests + all other store tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-30 00:03:03 +00:00
Vitor Pamplona 9bc1e3d3ce Merge pull request #2652 from vitorpamplona/claude/add-audience-to-stage-HhJgJ
Add tap gesture support for audience member promotion
2026-04-29 19:53:20 -04:00
Claude 544d26d9ec refactor(quartz): tidy projection package, flatten StoreEvent, per-filter limits
Five changes wrapped together:

1. Move ObservableEventStore + StoreEvent from store/observable/ into
   store/projection/. One package owns the projection bus + projection
   itself; consumers import only `store.projection.*`.

2. Flatten DeleteRule into the StoreEvent sealed type:
     StoreEvent.Insert(event)
     StoreEvent.DeleteByFilter(filters)
     StoreEvent.DeleteExpired(asOf?)
   ObservableEventStore.delete(filter|filters) emits DeleteByFilter;
   deleteExpiredEvents() emits DeleteExpired with the pinned cutoff.

3. Use event.address() for AddressableEvent and event.isExpirationBefore(t)
   for NIP-40 checks; drop the local addressOf(kind, pubKey, dTag) and
   isExpiredAt helpers. Plain replaceables (kind 0/3, 10000-19999) still
   build Address(kind, pubKey, "") manually since they don't implement
   AddressableEvent. NIP-09 delete-by-address now looks up the projection's
   byAddress index with the deletion's Address directly (it's already an
   Address, no conversion needed).

4. Per-filter limit. Each filter retains its own capped TreeSet<Slot>
   (sorted created_at DESC, id ASC). A slot is "live" iff at least one
   filter retains it. The projection's items is the deduped union, so
   when filter A and filter B match disjoint events the union can
   exceed any single filter's limit. New tests confirm both behaviours:
   per-filter eviction + union-larger-than-cap.

5. Drop the redundant + 1 in expiration checks; use isExpirationBefore
   directly with TimeUtils.now() for arrival-time guards. The sweep
   path subtracts 1 because the store's SQL uses strict `<` while the
   helper is `<=`.

17/17 projection tests + all other store tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-29 22:43:46 +00:00
Claude 208d9a8f7f feat(nests): tap an audience avatar to open the participant sheet
The host's primary path to add an audience member to the stage was the
long-press → "Promote to Speaker" row, which is hard to discover and was
also being swallowed by ClickableUserPicture (which ignores onLongClick
when onClick is null). Wire a tap handler on audience cells so a single
tap opens the same per-participant sheet, surfacing Promote to Speaker
for hosts and View Profile / Follow / Mute for everyone else.
2026-04-29 22:43:23 +00:00
Vitor Pamplona d44baa7e8f Merge pull request #2651 from vitorpamplona/claude/fix-windows-test-failure-dY1Jv
Fix race condition in MoQ subscription registration
2026-04-29 18:35:56 -04:00
Vitor Pamplona c8327e829f Merge pull request #2647 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-29 18:26:59 -04:00
Vitor Pamplona 31a2705173 Merge pull request #2649 from vitorpamplona/claude/fix-hand-raise-sync-Yf4b4
Fix stage presence sync and relay propagation in meeting rooms
2026-04-29 18:26:21 -04:00
Vitor Pamplona 997f52a31a Merge branch 'claude/fix-hand-raise-sync-Yf4b4' of https://github.com/vitorpamplona/amethyst into claude/fix-hand-raise-sync-Yf4b4 2026-04-29 18:19:22 -04:00
Claude 19b534a9e2 fix(nestsClient): register inbound moq-lite subscription before sending Ok
The publisher's inbound-bidi handler wrote SubscribeOk to the bidi
before calling registerInboundSubscription. The peer's first
publisher.send() after observing Ok could race the registration on
dispatchers that resume the peer's continuation before the handler's
(notably Windows under Dispatchers.Default), causing send to observe
an empty inboundSubs and return false. Reordering makes the peer's
view of Ok a happens-after of the registration.

Fixes the Windows-only failure in
MoqLiteSessionTest.publisher_acks_subscribe_and_pushes_group_data_on_uni_stream.
2026-04-29 22:14:23 +00:00
Claude 8d228db440 fix(nests): preserve relays tag on participant updates + sync action bar with stage grid
Two related bugs the user hit while testing against nostrnests:

1. Approving a hand-raise made the Hands tab disappear (the host's
   local app saw the role grant) but the audience member did not
   appear on stage anywhere else.

   Cause: RoomParticipantActions.rebuild() rebuilt the kind-30312
   without the original room's `relays` tag. The recently-added
   broadcast fan-out path (Account.computeRelayListToBroadcast) keys
   off `event.allRelayUrls()` for kind 30312, so a republished room
   event with no relays tag only reaches the host's outbox — not
   nostrnests's fixed five reads or the audience member subscribing
   on those reads. The audience member never sees their SPEAKER tag
   and stays absent from the participant list.

   Fix: copy `original.relays()` into the rebuild so every republish
   (approve, demote, kick → re-broadcast) keeps the relay routing
   that lets the room reach its full audience.

2. Tapping "Leave Stage" as the host emptied the StageGrid
   ("Waiting for speakers…") but the action bar still showed the
   Talk + Leave Stage cluster.

   Cause: two different definitions of "on stage" were in play.
   StageGrid uses ParticipantGrid (role + presence.onstage flag),
   so flipping onStageNow=false drops the host out. The action bar
   gated on the role-only `onStage: List<ParticipantTag>` (the
   host's HOST tag never goes away), so the cluster stayed.

   Fix: derive isOnStageMe from participantGrid.onStage so both
   surfaces share the same "stepped off" semantics. The host who
   taps Leave Stage now drops to the audience-style mute toggle
   and can rejoin (kind-10312 onstage flips back to 1) without
   the controls lying about their state.

https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh
2026-04-29 22:13:36 +00:00
Vitor Pamplona 9628d17f5f Improves the relay list used for linked Notes to include their relay urls 2026-04-29 21:48:32 +00:00
Claude b0e7c62bb5 fix(nests): broadcast presence/chat to the linked room's full relay list
Hand-raise toggles in Amethyst weren't appearing in nostrnests's UI.

nostrnests's NestsUI v2 routes reads against a fixed five-relay list
(NestsUI-v2/src/lib/const.ts: relay.snort.social, nos.lol, relay.damus.io,
relay.ditto.pub, relay.primal.net) — no outbox model. Its presence query
is just `kinds:[10312], "#a":[roomATag]` over those five relays.

Commit 637174ef already taught computeRelayListToBroadcast to fan a kind
30312 *room* event out to its `relays` tag. But kind 10312 presence (and
chat / reactions, etc.) is a different event type that *links* to the
room via an `a` tag. For those, broadcast was only reaching:

  - the broadcaster's outbox relays
  - the single firstOrNull() hint baked into the `a` tag
  - the relay we happened to receive the room event from

If none of those overlap with nostrnests's fixed five reads, the
hand-raise update is silently dropped.

Extend the `linkedAddressIds` block in computeRelayListToBroadcast: when
the linked address resolves to a MeetingSpaceEvent / MeetingRoomEvent /
LiveActivitiesEvent, also add that linked event's allRelayUrls(). This
covers presence updates and any other room-scoped event that points at
a Nest via `#a`.

https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh
2026-04-29 21:48:32 +00:00
Vitor Pamplona 9b583d99ff Improves the relay list used for linked Notes to include their relay urls 2026-04-29 17:44:11 -04:00
Claude f666fe2002 refactor(quartz): key projection's address index by Address instead of String
Renames `byStableKey: HashMap<String, Slot>` to
`byAddress: HashMap<Address, Slot>` in EventStoreProjection. Address
is a data class on every platform so equals/hashCode are derived
from (kind, pubKeyHex, dTag) — same identity as the synthetic string
key, but typed.

`stableKey(event)` / `stableKey(kind, pubKeyHex, dTag)` become
`addressOf(...)` returning `Address?`. Plain replaceables map to
`Address(kind, pubKey, "")`, addressables to
`Address(kind, pubKey, dTag)`, regular events to null.

`limit` was already used at insertNew() to enforce the cap on
inserts; left unchanged.

15/15 projection tests still pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-29 21:42:46 +00:00
Claude ecc1573606 fix(nests): broadcast presence/chat to the linked room's full relay list
Hand-raise toggles in Amethyst weren't appearing in nostrnests's UI.

nostrnests's NestsUI v2 routes reads against a fixed five-relay list
(NestsUI-v2/src/lib/const.ts: relay.snort.social, nos.lol, relay.damus.io,
relay.ditto.pub, relay.primal.net) — no outbox model. Its presence query
is just `kinds:[10312], "#a":[roomATag]` over those five relays.

Commit 637174ef already taught computeRelayListToBroadcast to fan a kind
30312 *room* event out to its `relays` tag. But kind 10312 presence (and
chat / reactions, etc.) is a different event type that *links* to the
room via an `a` tag. For those, broadcast was only reaching:

  - the broadcaster's outbox relays
  - the single firstOrNull() hint baked into the `a` tag
  - the relay we happened to receive the room event from

If none of those overlap with nostrnests's fixed five reads, the
hand-raise update is silently dropped.

Extend the `linkedAddressIds` block in computeRelayListToBroadcast: when
the linked address resolves to a MeetingSpaceEvent / MeetingRoomEvent /
LiveActivitiesEvent, also add that linked event's allRelayUrls(). This
covers presence updates and any other room-scoped event that points at
a Nest via `#a`.

https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh
2026-04-29 21:36:23 +00:00
Crowdin Bot 8dec96710f New Crowdin translations by GitHub Action 2026-04-29 21:31:01 +00:00
Vitor Pamplona f63e3b1c67 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-29 17:28:38 -04:00
Claude d394a38f2b feat(quartz): emit StoreEvent.Delete from observable for filter / expired sweeps
Replaces ObservableEventStore.events: SharedFlow<Event> with
SharedFlow<StoreEvent>, where StoreEvent is:

  Insert(event)
  Delete(rule: DeleteRule)
    DeleteRule.Filtered(filters)   ← from delete(filter) / delete(filters)
    DeleteRule.Expired(asOf)       ← from deleteExpiredEvents()

ObservableEventStore now emits Insert for accepted events, and Delete
for every out-of-band removal that went through it. The Expired rule
carries the cutoff timestamp (TimeUtils.now() at call time) so the
projection's in-memory drop matches the store's on-disk drop without
clock skew between collectors.

EventStoreProjection no longer runs its own NIP-40 ticker. Expired
events linger in [items] until the application calls
deleteExpiredEvents() on the observable — at which point the
projection drops everything whose expiration is past `asOf`. The
ticker, expirationTickMs constructor arg, and sweepExpired helper are
all removed.

For DeleteRule.Filtered the projection iterates current slots and
drops any whose event matches any of the rule's filters via
Filter.match — same predicate the store uses.

Tests:
- nip40ExpirationDroppedByTicker → nip40ExpirationDroppedOnStoreSweep:
  drives a deleteExpiredEvents() call instead of waiting on a ticker.
- New deleteByFilterRemovesMatchingSlots: confirms delete(filter) on
  the observable removes exactly the matching slots from the projection
  without touching others.
- 15/15 projection tests + all other store tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-29 21:20:31 +00:00
Vitor Pamplona b77407f13d Merge pull request #2648 from vitorpamplona/claude/fix-nest-room-connection-H48ad
test(quic): add concurrent producer/consumer regression for SendBuffer
2026-04-29 17:19:17 -04:00
Vitor Pamplona df98235d31 Minor adjustments to remove warnings 2026-04-29 17:16:14 -04:00
Claude a84fbd2e57 test(quic): add concurrent producer/consumer regression for SendBuffer
The previous SendBuffer suite (FlowControlEnforcementTest) is entirely
single-threaded — every test calls enqueue and takeChunk sequentially
on the same coroutine, so the race that crashed the audio path in
production (NoSuchElementException from chunks.first() under
concurrent enqueue + takeChunk) stayed invisible. The whole :quic
commonTest tree had no concurrent test at all.

Three new tests run real-thread races on Dispatchers.Default:

  - concurrent_enqueue_and_takeChunk_does_not_throw drives multiple
    producer coroutines + a consumer coroutine and asserts the buffer
    drains cleanly with no exception.
  - concurrent_takeChunk_callers_never_double_drain_a_chunk fans out
    multiple consumers against a pre-populated buffer; asserts the
    sum of bytes handed out equals the bytes enqueued (i.e. no chunk
    is double-counted by overlapping head-peel paths).
  - concurrent_finish_with_inflight_enqueue_emits_correct_fin races
    finish() against in-flight writes and asserts the FIN comes
    AFTER every enqueued byte.

Tests pass against the synchronised SendBuffer; running them against
the pre-fix unsynchronised version corrupts state badly enough that
the consumer wedges (an explicit "this is what the bug looked like"
demonstration). With internal synchronisation in place the suite
finishes in <0.2 s.

Documents the concurrent-access contract so a future "let's drop the
sync, it's hot" refactor immediately fails CI.
2026-04-29 21:08:03 +00:00
Claude 32850c3d40 refactor(quartz): make ObservableEventStore an external composition layer
Reverts the previous round's choice of embedding an ObservableEventStore
inside EventStore. The wrapper now lives strictly outside any specific
store: callers compose `ObservableEventStore(EventStore(...))` (or
`ObservableEventStore(FsEventStore(...))`, or any IEventStore) and use
that as their projection bus.

EventStore is back to a plain IEventStore implementation over
SQLiteEventStore — no `observable`, `events`, or `observe()` on it.

ObservableEventStore gains the `observe(filters, relay, scope)` and
`observe(filter, relay, scope)` convenience methods. NIP-62 vanish
scoping is passed in per-projection (rather than a constructor field
on the wrapper) so the same observable can feed projections with
different relay scopes.

Tests construct `observable = ObservableEventStore(store)` in
`setUp` and route inserts through `observable.insert(...)` so the
projection bus actually publishes them. 14/14 projection + 219/219
other store tests pass.

https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5
2026-04-29 21:06:27 +00:00
Vitor Pamplona c88183809b Merge pull request #2646 from vitorpamplona/claude/fix-nest-room-connection-H48ad
Align nests servers with NIP-53 spec: separate relay and auth URLs
2026-04-29 17:03:27 -04:00
Claude 2d45c6ff4a fix(quic): make SendBuffer thread-safe to stop torn-state crash
QuicConnectionDriver.sendLoop holds the connection mutex while it calls
SendBuffer.takeChunk via QuicConnectionWriter.drainOutbound, but
WtPeerStreamDemux's per-stream `send` callback calls
SendBuffer.enqueue from arbitrary application coroutines without the
connection lock. The two paths concurrently mutate
(chunks, pendingBytes, headOffset, finPending, finSent, sentEnd,
nextOffset). Under load this surfaced as

  java.util.NoSuchElementException: ArrayDeque is empty.
    at kotlin.collections.ArrayDeque.first(ArrayDeque.kt:102)
    at com.vitorpamplona.quic.stream.SendBuffer.takeChunk(SendBuffer.kt:85)
    at com.vitorpamplona.quic.connection.QuicConnectionWriterKt.buildApplicationPacket(...)
    at com.vitorpamplona.quic.connection.QuicConnectionDriver.sendLoop(...)

The writer saw `pendingBytes > 0` (incremented by an in-flight
enqueue on another thread) before the matching `chunks.addLast`
became visible, fell into the head-peel branch, and tripped on
chunks.first().

Wrap every read and write of SendBuffer state in `synchronized(this)`,
including the cheap `readableBytes` / `sentOffset` / `finPending` /
`finSent` getters used by the writer's pre-flight checks (so they
can't read torn state either). The lock is uncontended in the common
case and short-held in the rare race; we already use synchronized
blocks elsewhere in commonMain (QuicConnectionDriver.kt).
2026-04-29 20:56:33 +00:00
Claude 2fae726e5e fix(nests): place Audience/Hands tab counter beside the label, not over it
PrimaryTabRow's BadgedBox places the count badge in the top-end corner
of its anchor, which on tight tab labels lands directly on top of the
text. Lay them out as a Row instead so "Audience  3" reads cleanly,
matching the tabbar comment's "Hands · 3" intent.
2026-04-29 20:53:38 +00:00
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