Commit Graph

12710 Commits

Author SHA1 Message Date
Vitor Pamplona bf82ce454b Merge pull request #2707 from mstrofnone/feat/namecoin-import-nip05-only
feat(namecoin): resolve `import` items per ifa-0001 (NIP-05 only)
2026-05-03 12:20:41 -04:00
David Kaspar 6085cbaf6b Merge pull request #2713 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-03 18:20:11 +02:00
Vitor Pamplona 910f6e36ca Merge pull request #2709 from mstrofnone/feat/electrumx-relay-testls-bit-direct
feat(electrumx): add bare IP peer for relay.testls.bit (23.158.233.10)
2026-05-03 12:19:36 -04:00
Vitor Pamplona c122d99a5b Merge pull request #2705 from davotoula/feat/desktop-quartz-enhancements
desktop: verify event signatures on receive
2026-05-03 12:19:07 -04:00
Crowdin Bot a73324baac New Crowdin translations by GitHub Action 2026-05-03 16:18:32 +00:00
Vitor Pamplona 401ef86810 Merge pull request #2711 from davotoula/feat/save-pdf-and-fix-mime
Save downloads: validate Content-Type & support PDF downloads
2026-05-03 12:16:55 -04:00
davotoula 81d4898f3a Allow PDF downloads, route them to Downloads/Amethyst
expose Save-to-Disk button for PDFs
add Save-to-Phone row for PDFs
2026-05-03 17:54:43 +02:00
Claude 6383a001eb Reject bogus Content-Type when saving downloaded media
Code review:
- also validate URL-extension fallback as a media type
- hoist trimInlineMetaData + drop !! on mimeType
2026-05-03 17:52:49 +02:00
mstrofnone 07f6c352e8 feat(electrumx): add bare IP peer for relay.testls.bit (23.158.233.10)
Adds the underlying IP literal as a separate ElectrumX entry in both
the clearnet default list and the Tor list, mirroring how
46.229.238.187:57002 sits beside nmc2.bitcoins.sk:57002.

Why a bare IP entry alongside a hostname entry?

  - Robustness against unhealthy ICANN DNS paths. The relay.testls.bit
    name resolves through Namecoin (.bit), which most resolvers ship
    via a NIP-05 lookup against ElectrumX itself \u2014 chicken-and-egg if
    the only listed servers ARE .bit-resolved. A bare IP gives the
    resolver pool a completely-DNS-free anchor.
  - Same physical box, same self-signed cert, same SHA-256 (DER) pin
    (bb0b35e6\u20266cba) added in the prior commit, so no new cert pin is
    needed.

The .onion entry added in the prior commit already serves as the
DNS-free Tor anchor; this commit handles the equivalent for clearnet.

Verified end-to-end:
  node tls direct connect 23.158.233.10:50002 (no SNI) -> server.version
  reply ElectrumX 1.16.0; cert SHA-256 matches the pinned hash exactly.
2026-05-03 17:37:10 +10:00
mstrofnone 737464e528 feat(electrumx): add relay.testls.bit endpoints (clearnet TLS + Tor)
Adds the relay.testls.bit ElectrumX-NMC deployment as a second public
Namecoin name resolver. The same box also runs the Namecoin-anchored
Nostr relay at wss://relay.testls.bit/, so a single TLSA record on
d/testls anchors the trust chain for both services.

Default server set (clearnet):
  + relay.testls.bit:50002      (TLS, self-signed cert pinned)

Tor server set (onion-first):
  + 6cbn4rsk...onion:50001      (plaintext over Tor; re-uses the
                                 relay's existing v3 hidden service,
                                 onion key already authenticates)
  + relay.testls.bit:50002      (clearnet TLS fallback)

Cert pin (SHA-256 of DER):
  bb0b35e64235a794e157b69acd72da4ccc16a4d81d0ff6b1d8f7fdfc6cca6cba

The new self-signed cert is appended to PINNED_ELECTRUMX_CERTS so
strict-TLS Android devices (Samsung One UI 7, GrapheneOS) accept it
without a trust-all fallback.

Reference deployment also exposes wss://relay.testls.bit/electrumx
(WebSocket transport for browser-based Nostr clients), which inherits
the relay's existing TLSA-pinned ECDSA cert from d/testls. The current
JVM ElectrumXClient is TCP+TLS only; adding a WS variant of the client
is a larger change and tracked separately.
2026-05-03 17:23:53 +10:00
mstrofnone 3835622fc5 feat(namecoin): resolve import items per ifa-0001 (NIP-05 only)
Adds a new `NamecoinImportResolver` that follows the `import` field of a
Namecoin Domain Name Object value, recursively merging the imported
values into the importing object before `NamecoinNameResolver` reads
the `nostr` field.

Why
---
The 520-byte per-name limit on Namecoin is tight when a single record
needs `ip`, NIP-05 names, TLS, and per-subdomain map entries. A common
workaround (used by `testls.bit` and others) is to keep the apex record
small by delegating shared blocks into a sibling name via
`"import":"dd/<name>"` per ifa-0001 §"import". Without import support,
NIP-05 search for `testls.bit` (and the like) fails: the resolver sees
no `nostr` field at `d/testls` and gives up before consulting the
imported `dd/testls` that actually carries the names block.

What
----
Per ifa-0001 §"import":
  * Importer items take precedence over imported items (including `null`,
    which suppresses the imported counterpart).
  * Recursion is supported up to 4 levels (the spec minimum) by default.
    Cycles are broken by a visited-set; over-budget chains are silently
    truncated, with the importer's own items still taking effect.
  * Multiple imports in one array merge later-wins (the array's own
    order), with the importing object on top of all of them.
  * Subdomain selectors (the optional 2nd element of each inner array)
    are resolved through a `map` walk inside the imported value, with
    the standard exact-label > `*` wildcard > `""` default rules.
  * Three short-hand value forms are accepted alongside the canonical
    array-of-arrays: `"import":"d/foo"`, `"import":["d/foo"]`, and
    `"import":["d/foo","selector"]`. They map onto the canonical
    representation losslessly.
  * A failed import lookup (name not found, malformed JSON, network
    error) is treated as `{}` rather than failing the whole record, so
    transient ElectrumX hiccups don't kill resolution. The importing
    object's own items still apply.

Wires the expander into `NamecoinNameResolver.performLookup` and
`performLookupDetailed`. Records without an `import` key skip the
resolver entirely (zero extra I/O), so non-import names pay no cost.

Tests
-----
20 new tests in `NamecoinImportTest`:
  * Unit (resolver in isolation): no-import passthrough, all four
    shorthand value forms, importer precedence, null suppression,
    depth-4 recursion happy path, over-budget truncation, lookup
    failure, malformed JSON, malformed `import` value, cycle
    protection, multi-label selector descending in DNS order, wildcard
    fallback.
  * Integration with `NamecoinNameResolver`: bare and named NIP-05 across
    an import (mirroring the real-world `testls.bit -> dd/testls`
    deployment); regression guard that no-import records issue exactly
    one ElectrumX query; importer-wins on `nostr.names`; lenient
    failure when the imported boilerplate is unreachable;
    `resolveDetailed` returns `Success` and `NoNostrField` correctly
    across imports.

All 15 existing `NamecoinNameResolverTest` cases continue to pass.

Scope note
----------
This PR is intentionally scoped to the NIP-05 / search path. The
relay-resolution path (#2595) also benefits from import support; that
wiring is left to land alongside #2595 so the two PRs can move
independently.
2026-05-03 09:53:10 +10:00
davotoula 3dc79ac6a5 Code review:
rethrow CancellationException; offload NWC verify off WS thread
align verification API with Amethyst Android
2026-05-02 18:42:09 +02:00
davotoula 68fb6fb703 desktop: quartz enhancements 2026-05-02 18:42:09 +02:00
Vitor Pamplona 3554ccf4c0 Merge pull request #2704 from davotoula/fix/quartz-nip19-tlv-bounds
fix(quartz): bound Tlv.parse against malformed naddr/nevent/nprofile
2026-05-02 10:00:50 -04:00
David Kaspar 21c92908cf Merge pull request #2703 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-02 15:40:14 +02:00
Crowdin Bot 7ed618e1f7 New Crowdin translations by GitHub Action 2026-05-02 13:39:52 +00:00
davotoula dc1ef53027 Code review:
- switch Tlv.parse to integer cursor
- tighten Tlv.parse loop + drop tautological fuzz assertion
2026-05-02 15:39:06 +02:00
davotoula ecc79192a4 bound Tlv.parse — guard short input + clamp truncated lengths 2026-05-02 15:38:34 +02:00
davotoula 16c30da4d6 chore: gitignore .claude/scheduled_tasks.lock 2026-05-02 15:37:25 +02:00
David Kaspar 49ce010a96 Merge pull request #2702 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-02 14:44:23 +02:00
Crowdin Bot d622f2464e New Crowdin translations by GitHub Action 2026-05-02 11:58:02 +00:00
Vitor Pamplona efe8cc7545 Merge pull request #2697 from davotoula/fix/pow-string-index-out-of-bounds
Off-by-one error in PoW evaluator
2026-05-02 07:56:30 -04:00
David Kaspar cd8bf9fdd7 Merge pull request #2699 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-02 09:29:19 +02:00
davotoula ca640df2c2 cover PoWRankEvaluator partial-rank branches explicitly
bound PoWRankEvaluator hex loop to id.indices
2026-05-02 09:27:39 +02:00
Crowdin Bot d4808d585a New Crowdin translations by GitHub Action 2026-05-02 07:26:22 +00:00
David Kaspar 412ba1afe2 Merge pull request #2698 from davotoula/investigate/ci-context-cast-to-activity
fix: use LocalActivity to resolve NestActivity (lint ContextCastToActivity)
2026-05-02 09:24:51 +02:00
davotoula 59275f6638 fix: use LocalActivity to resolve NestActivity (lint ContextCastToActivity)
Casting LocalContext.current to NestActivity tripped the Jetpack
Activity Compose lint rule ContextCastToActivity, failing
:amethyst:lintPlayBenchmark in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:40:34 +02:00
Vitor Pamplona 656614c423 Merge pull request #2696 from vitorpamplona/claude/moq-speaker-animation-okK2f
feat: animate local speaker ring from MoQ frame transmission
2026-05-01 22:03:18 -04:00
Claude 5d955eff99 feat: animate local speaker ring from MoQ frame transmission
Wire the local user's avatar into the existing speaking-ring animation so
the host/speaker sees the same green ring + amplitude glow on their own
cell that remote speakers already get.

Ground truth is the broadcaster's `publisher.send(opus)` success: the new
`onLevel` callback fires only after a frame actually leaves on the wire,
computed from the raw PCM peak (shared `peakAmplitude` util used by both
the player decode loop and the broadcaster capture loop). This means the
animation reflects what the relay sees, not any UI-side mute / role
state that could be stale or buggy — if frames are flowing, the ring
plays.

Plumbing: `NestsSpeaker.startBroadcasting` gains an optional `onLevel`
that the moq-lite + IETF broadcasters both honour, the reconnecting
wrapper replays it on every session reissue (alongside the existing
mute-intent replay), and `NestViewModel.startBroadcast` hands it a
lambda that calls the existing `onSpeakerActivity` / `onAudioLevel`
plumbing keyed on the local pubkey. The 250 ms speaking-timeout fades
the ring naturally when the user mutes or stops broadcasting.
2026-05-02 01:17:40 +00:00
Vitor Pamplona 0c4def6491 Merge pull request #2695 from vitorpamplona/claude/fix-duplicate-mic-buttons-AKgfT
fix: drop redundant StopBroadcastButton from nest action bar
2026-05-01 18:20:19 -04:00
Claude 770af8faf9 fix: tear down nest session on PIP close + Just Leave
Three more leave paths now fully release the speaker session and
listener (and with them AudioRecord + the system mic indicator)
before the Activity destruction queue eats viewModelScope:

  - PIP overlay's Leave action — pipReceiver invokes a cleanup
    callback set by NestActivityBody, then finish().
  - PIP swipe-dismiss — NestActivity.onStop() invokes the same
    callback when in PIP and isFinishing. Non-PIP onStop stays a
    no-op so backgrounding doesn't tear down.
  - Host's Just leave — calls viewModel.leave() before onLeave().
    Unlike Close the Room, this still leaves the kind-30312
    meeting space open for other users.

The cleanup callback is registered via DisposableEffect in
NestActivityBody so it's auto-cleared when the composable leaves.
2026-05-01 22:05:23 +00:00
Claude 36152243ab fix: tear down nest session on host Close the Room
The host-leave confirmation dialog's Close the Room button only called
finish(), relying on VM.onCleared() to release the AudioRecord. That
runs late in the destroy lifecycle, so the system mic-in-use indicator
stayed lit while the activity was queued for destruction.

Adds NestViewModel.leave(), which mirrors onCleared() (sets closed,
runs both teardowns with finalCleanup=true so closes route through
cleanupScope/GlobalScope and survive Activity destruction). Wires it
into the Close the Room callback only — the Just Leave and non-host
Leave paths are unchanged per request.
2026-05-01 21:47:33 +00:00
Vitor Pamplona 27250fa6dd Merge pull request #2694 from vitorpamplona/claude/redesign-nests-cards-AHCcK
Add section headers and compact view for Nests feed
2026-05-01 17:41:40 -04:00
Claude 6610683a20 fix: hide StageControlsBar on local intent flag for responsive tap
After tapping Leave the Stage, the bar lingered because it was gated
solely on isOnStageMe — a value derived from the presence aggregator.
That aggregator only updates after a presence event signs, broadcasts
to relays, and loops back through LocalCache. With the recent
broadcast / mute traffic contending on the signer plus the 500 ms
mute debounce in NestPresencePublisher, the round-trip ran long
enough that the user perceived Leave the Stage as not firing.

AND the visibility gate with ui.onStageNow (the local intent flag,
which flips synchronously on setOnStage(false)) so the tap hides the
bar on the next frame. Host demotion still hides it via the existing
isOnStageMe path.
2026-05-01 21:30:05 +00:00
Claude 7328874c09 fix(nests): wire kebab to NoteDropDownMenu instead of QuickAction
The kebab on a NoteCompose card opens the comprehensive
NoteDropDownMenu (Follow/Unfollow, Copy text/pubkey/note id, Share,
Edit, Broadcast, Timestamp, Pin, Bookmark, Delete/Report) — not the
smaller LongPressToQuickAction popup. The previous nests commit
wired the kebab to the wrong menu.

Replace the manual ClickableBox + VerticalDotsIcon + showQuickAction
plumbing with the existing MoreOptionsButton composable, which
already wraps the icon and triggers NoteDropDownMenu for both card
variants. Long-press still opens NoteQuickActionMenu via the outer
LongPressToQuickAction wrapper, matching NoteCompose's two-tier
gesture model exactly.

Drops the onMore parameter threading on ObserveAndRenderSpace,
RenderLiveSpacesThumb, and SpaceHostAndReactions — MoreOptionsButton
is self-contained, so the trailing optional parameter is no longer
needed.
2026-05-01 21:27:27 +00:00
Claude 00de615733 feat(nests): add visible kebab menu to room cards
Long-press on the cards already opened the standard
NoteQuickActionMenu, but the affordance was hidden — especially on
the compact ENDED card which has no visible interactive elements.

Add an explicit MoreVert (VerticalDotsIcon) tap target that opens
the same menu:

- ENDED compact card: 40dp icon on the trailing edge of the row.
- LIVE/SCHEDULED full card: 35dp icon in the existing
  SpaceHostAndReactions row, after Like and Zap.

Both wire through the showQuickAction lambda from
LongPressToQuickAction, so the icon and the long-press gesture
trigger the exact same menu (share, copy, broadcast, report,
block, delete) — no second popup-state plumbing.

ObserveAndRenderSpace, RenderLiveSpacesThumb, and
SpaceHostAndReactions gain an optional `onMore: (() -> Unit)?`
parameter so other callers (e.g. existing live-stream surfaces
that reuse SpaceHostAndReactions) keep their current rendering
unchanged when they don't pass one.
2026-05-01 21:20:44 +00:00
Claude c981058a9f fix: render StageControlsBar inside the Stage card surface
The previous commit placed the controls strip as a sibling row right
after StageGrid. Visually it read as a separate band, not as part of
the stage card.

Adds a bottomBar slot to StageGrid that renders inside the same
Surface, below the grid, sharing the card's tonal background and
horizontal padding. StageControlsBar drops its own horizontal padding
since the card supplies it, and keeps a top inset so it has breathing
room from the avatars.
2026-05-01 21:16:38 +00:00
Claude 1ce0e3c179 feat(nests): bucket feed into Live / Scheduled / Recently ended sections
Replace the flat NestsScreen list with three explicit sections so the
audio-room surface separates actionable rooms from historic ones:

- LIVE: ordered by follows-participating DESC, total-participants DESC,
  createdAt DESC. PRIVATE rooms continue to live in the LIVE bucket
  since they're audible once a join token is granted.
- SCHEDULED: ordered by `starts` ASC (soonest first). PLANNED rooms
  now have their own bucket instead of collapsing into ENDED.
- ENDED: ordered by createdAt DESC (proxy for "ended at"), capped to
  the last 7 d, and rendered with a compact one-line card variant
  (square thumb + name + host + "Ended Xh ago"). Tapping still
  routes through the read-only lobby so recordings remain reachable.

The DAL exposes `NestsFeedFilter.bucketOf()` so the screen can walk
the pre-sorted feed once and inject sticky-header sections without
re-sorting on each recomposition.

Cards now wrap in `LongPressToQuickAction`, giving share / copy /
delete / report / block / broadcast on long press — matching the
NoteCompose interaction model and giving hosts a path to delete
(kind:5) ended rooms without entering the room first.
2026-05-01 21:09:01 +00:00
Claude 4af15a301f Merge remote-tracking branch 'origin/main' into claude/fix-duplicate-mic-buttons-AKgfT 2026-05-01 21:01:50 +00:00
Claude 9c26c51a1b fix: move on-stage controls out of action bar onto stage card
NestActionBar held the speaker controls (Talk / MicMute / Leave the
Stage) alongside the audience controls (hand-raise / react / leave
room), making the bar uncomfortably wide on phones once a user was
promoted.

Splits responsibilities:
  - NestActionBar now only handles connection state in the start
    cluster (Connect / Connecting chip / Reconnecting chip / empty)
    plus the existing end cluster.
  - StageControlsBar is a new strip rendered under the Stage card,
    gated only on isOnStage, holding the full broadcast state machine.

isOnStage is the single visibility signal — a stable derived value
from the kind-30312 role + presence onstage flag — so connection
blips, broadcast state churn, mute toggles, and permission flows
never make the new bar appear or disappear. It only flips on a real
promote/demote or the user tapping Leave the Stage.
2026-05-01 20:57:07 +00:00
Vitor Pamplona a69e4696a8 Merge pull request #2693 from vitorpamplona/claude/fix-nests-audio-issue-a8sFv
Switch audio playback to USAGE_MEDIA for hands-free rooms
2026-05-01 16:53:38 -04:00
Claude 523e8a92ec fix(nests): play audio-room output through STREAM_MUSIC, not STREAM_VOICE_CALL
`AudioTrackPlayer` was constructing its `AudioTrack` with
`USAGE_VOICE_COMMUNICATION` + `CONTENT_TYPE_SPEECH` so the room would
behave like a phone call (volume rocker controls call volume, ducks
notifications). The `AudioTrack` then renders on `STREAM_VOICE_CALL`,
which Android only services audibly while the device is in
`MODE_IN_COMMUNICATION`. Nests never drives `AudioManager.mode` (only
NIP-100's `CallAudioManager` does), so on most devices the playback
either dropped to the earpiece at near-zero volume or produced no audio
at all — making rooms appear silent on both phones.

Fix: route through `USAGE_MEDIA` + `CONTENT_TYPE_SPEECH` (=
`STREAM_MUSIC`) so audio-room playback comes out of the loudspeaker by
default and the volume rocker controls it any time. Same approach
Twitter/X Spaces and Clubhouse take for hands-free audio rooms. Echo
cancellation still works on the capture side via
`MediaRecorder.AudioSource.VOICE_COMMUNICATION` regardless of the
playback usage.

Also update `NestForegroundService.requestAudioFocus` to request focus
under the matching `USAGE_MEDIA` attributes so the focus claim and the
playback attributes line up.
2026-05-01 20:50:12 +00:00
Claude e7cb6dae5f fix: drop redundant StopBroadcastButton from nest action bar
MicMuteToggle already covers 'stop sending audio while staying on stage'
with sample-accurate resume, and LeaveStageButton covers full teardown.
StopBroadcastButton drew a Mic icon next to MicMuteToggle's Mic icon,
reading visually as two adjacent mic buttons.
2026-05-01 20:39:11 +00:00
Vitor Pamplona e6d755a7ab Merge pull request #2692 from vitorpamplona/claude/fix-encryption-deprecation-Hu4T2
Fix flaky test by waiting for mute state instead of start count
2026-05-01 16:31:20 -04:00
Claude 7c1af48ae7 fix(nestsClient): wait for mute replay in setMuted_intent_replays test
ScriptedSpeaker publishes startCount > 0 from inside startBroadcasting,
so polling startCount alone races the wrapper pump's subsequent
`if (desiredMuted) handle.setMuted(true)` step. Under load (full suite
run on CI) the assertion fired before the pump replayed mute intent,
producing setMutedCalls=0. Wait for the post-condition the assertion
checks instead.
2026-05-01 20:26:18 +00:00
Vitor Pamplona 66985020e0 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-01 16:16:58 -04:00
Vitor Pamplona 4a54ac6bee minimizes chance of failing tests 2026-05-01 16:16:14 -04:00
Vitor Pamplona 653ad9992b unecessary tag 2026-05-01 16:16:00 -04:00
Vitor Pamplona a6fef4c813 Merge pull request #2691 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-01 16:03:42 -04:00
Crowdin Bot d3057b77e1 New Crowdin translations by GitHub Action 2026-05-01 20:01:13 +00:00