Commit Graph
18014 Commits
Author SHA1 Message Date
davotoula 42bf2a43d8 fix: address code-review findings on the buzz CLI + wrapper scripts
Three findings from the review of the Sonar literal-extraction pass. None were
introduced by that pass — it renamed or sat next to each one.

no_relays told the wrong story. `relaysFor` returns an empty *non-null* set when
--relays was given but every entry failed `normalizeGroupRelay`, so the elvis to
`outboxRelays()` never fires and the user was told to pass the flag they had just
passed. The message now names the real problem and echoes the rejected value; the
`no_relays` error code is unchanged.

`participants` was string-munged. `arr.toString().trim('[',']').split(",")` turns
any shape other than a flat string array into plausible-looking fake pubkeys, and
splits a comma-bearing string into two entries. Now decoded as a JsonArray, with
non-string elements skipped rather than stringified. Extracted to
`participantsOf` so it is testable; BuzzParticipantsTest covers the shapes and
was confirmed to fail (3 of 5 cases) against the old implementation.

Nothing kept the two copies of the buzz-agent wrappers in sync. They exist as
byte-identical trees under cli/src/main/resources/buzz-agent (what `agent up`
extracts) and tools/buzz-agent (what the README tells people to use), with no
build step or test pinning them — I had to hand-apply the same patch twice this
week. BuzzAgentWrapperSyncTest now asserts it. The tools/ tree is declared as a
`:cli:test` input, without which Gradle calls the task up-to-date after a
tools/-only edit and misses exactly the drift being guarded (verified both ways).
2026-07-29 15:42:20 +02:00
Vitor Pamplona 054dffd55d fix(ci): make the Homebrew/Winget bump workflows actually fire
All four bump workflows triggered on `release: types: [released]`, which never
fires here: create-release.yml publishes the release with GITHUB_TOKEN, and
GitHub suppresses workflow-triggering events for GITHUB_TOKEN actions. They had
zero runs across every release up to v1.13.1.

Switch them to `workflow_run` on "Create Release Assets" completion, filtered to
a successful tag push. That also removes a latent race: `released` fired while
the matrix legs were still uploading assets, whereas workflow_run fires after
all of them finish.

The workflow_run payload carries no draft/prerelease flags, so add a
resolve-release composite action that reads them back from the API and feeds
assert-stable-release, keeping the defense-in-depth guard intact instead of
inferring stability from the tag string alone.

Also gate the cask/winget bumps on the package existing upstream. Neither
`amethyst-nostr` nor `VitorPamplona.Amethyst` has been bootstrapped, and
bump-cask-pr/winget-releaser can only update an existing package — without the
gate, fixing the trigger would file a spurious [release-ops] issue on every
release.

Docs: correct the claims this uncovered — Homebrew/Winget are not shipping,
macOS is arm64-only (no Intel DMG), the release carries 31 assets (13 Android,
not 12), Maven Central publishes from a step inside deploy-android and lags
repo1 by tens of minutes, RELEASE_NOTES_ID is minor-releases-only, and note the
git-credential-manager hang that blocks the release push.
2026-07-29 09:37:24 -04:00
Vitor PamplonaandGitHub 7d26c06fe9 Merge pull request #3799 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-29 09:10:52 -04:00
Vitor PamplonaandGitHub 0ae90dbdc6 Merge pull request #3797 from davotoula/fix/sonar-case-default-clause
Harden workflow-ship.sh and pin it with a regression harness
2026-07-29 09:09:03 -04:00
vitorpamplonaandgithub-actions[bot] 8d877ea70d chore: sync Crowdin translations and seed translator npub placeholders 2026-07-29 12:33:41 +00:00
Vitor PamplonaandGitHub 7ba5631ec3 Merge pull request #3796 from davotoula/fix/hls-strip-ll-tags
Work around a fatal media3 crash on Low-Latency HLS (LL-HLS), eg NoGoodRadio
2026-07-29 08:30:39 -04:00
Vitor PamplonaandGitHub 1133560dd6 Merge pull request #3795 from nrobi144/feat/desktop-profile-parity
Desktop: profile screen parity — tabs, header actions, banner/bio, zaps
2026-07-29 08:30:02 -04:00
David KasparandGitHub 842a01ea2f Merge pull request #3794 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-29 14:28:47 +02:00
davotoula fcbd9d727b test: add a regression harness for workflow-ship.sh 2026-07-29 14:14:39 +02:00
davotoula 30c7dbc2f3 Code review:
1. `title="$(git log -1 … | cut …)"` aborts the whole script under
   `set -euo pipefail` when the worktree has no commits
2. `base_branch` only fell back to `main`
2026-07-29 14:14:28 +02:00
davotoula 4700e51d82 fix: add explicit default case to the workflow-ship branch guard 2026-07-29 14:13:51 +02:00
davotoulaandClaude Opus 5 c26988f399 Revert "TEMP: HlsVerify error-level probes for on-device verification"
This reverts commit 76b3323583. The probes did their job.

Verified on a Pixel 9a / Android 17 against beam.mapboss.co.th — a
third-party LL-HLS packager, unrelated to zap-stream-core, so this is not
tuned to one vendor's output:

  PARSE STRIPPED 2360B of 3426B  chunklist_0_video_..._llhls.m3u8
  PARSE STRIPPED 3002B of 4068B  chunklist_2_audio_..._llhls.m3u8

26 reloads of each rendition over 50s of continuous playback, ~70% of every
playlist removed, and zero DataSpec.subrange occurrences — the crash that
androidx/media#3350 tracks.

Both branches confirmed. Eight ordinary HLS streams logged "no LL tags" and
took the identity path untouched, so the stripper is inert on normal
playlists. Routing was correct on ~15 distinct real URLs:
`hls=true -> HlsMediaSource` for application/x-mpegURL, and
`hls=false -> ProgressiveMediaSource` for video/mp4 — which is the
isHlsMediaItem coverage that unit tests cannot provide.

The 18 ERROR states in the same capture were all unrelated: dead URLs
(403/404/502/504), four UnknownHostException from a network drop, and two
UnrecognizedInputFormatException from a youtu.be link on the progressive
path, which ExoPlayer has never been able to play directly.

The pure predicates keep their unit tests, which are the durable guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uE2Db4pzmhi5cFKKkyq2m
2026-07-29 13:56:15 +02:00
davotoulaandClaude Opus 5 76b3323583 TEMP: HlsVerify error-level probes for on-device verification
DO NOT MERGE — revert this commit before the branch goes anywhere.

PLAYBACK_DIAG_TAG logs at DEBUG and Amethyst.onCreate sets
`Log.minLevel = if (BuildConfig.DEBUG) DEBUG else ERROR`, so nothing on that
tag survives in the benchmark build. Three logcat captures during this work
had zero PlaybackDiag lines for exactly that reason, which left the routing
decision unverifiable — the fix was only ever confirmed indirectly, by the
crash no longer happening.

Adds one ERROR-level tag, HlsVerify, on the three paths unit tests cannot
reach:

  - CustomMediaSourceFactory.createMediaSource — the routing decision, so
    isHlsMediaItem's verdict and the chosen factory are visible. This is the
    predicate with no test coverage, since android.net.Uri stubs to null
    under unitTests.isReturnDefaultValues.
  - LowLatencyStrippingParser.parse — whether the stripper actually engaged
    and how many bytes it removed, so a silent stop-stripping regression is
    visible rather than inferred.
  - HlsLivenessRecorder.maybeRecord — the other consumer of the shared
    predicate, unreachable without a Player.

The pure predicates underneath are already unit-tested and remain the durable
regression guard; this is only for confirming the wiring on a device.

Capture with `adb logcat -s HlsVerify:E`. Remove with `grep -rn HlsVerify` or
by reverting this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uE2Db4pzmhi5cFKKkyq2m
2026-07-29 13:37:16 +02:00
davotoula cabe539e8e Code review:
- retire the `.m3u8` substring predicate across the UI
- cover the preload hint and the byte/charset layer
- share one HLS predicate with the liveness recorder
- unify the HLS predicate and track the media3 bug upstream
2026-07-29 13:30:36 +02:00
davotoula 6f5e0c6a6b fix(playback): strip Low-Latency HLS tags before media3 parses the playlist
media3 crashes fatally on an LL-HLS playlist whose parts are byte ranges —
what zap-stream-core emits (`#EXT-X-PART:...,BYTERANGE="359712@0"`).
Reproduced on media3 1.10.1, Pixel 9a / Android 17, ~0.6s after
ExoPlayerImpl.Init:

    IllegalArgumentException
      at DataSpec.<init>            // checkArgument(length > 0 || length == LENGTH_UNSET)
      at DataSpec.subrange
      at HlsMediaChunk.feedDataToExtractor
      at HlsMediaChunk.loadMedia
2026-07-29 13:30:36 +02:00
nrobi144andClaude Opus 4.8 c5ba33486a feat(desktop): format profile zap amounts compactly (210k, 1.5M)
Use commons showAmount() for the Zaps tab total and each zapper row instead of
raw sats, matching Amethyst Android and other Nostr clients.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:57 +03:00
nrobi144andClaude Opus 4.8 c204e5b485 fix(desktop): label the profile 'Mutual' tab 'Yours' to match Android
The filter (your own posts that mention this profile) matches Android's
UserProfileMutualFeedFilter, but Android's user-facing string is 'Yours'
(<string name="mutual">Yours</string>). The internal filter name stays
DesktopMutualFeedFilter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:56 +03:00
nrobi144andClaude Opus 4.8 1b70f0939d fix(desktop): group profile header actions + load Followers/Following metadata
1. Header actions were direct children of a SpaceBetween Row, so the overflow
   (⋮) menu floated to the center. Group Edit/Follow/⋮ in their own Row and put
   the overflow last (after the primary Follow action), per convention. Extract
   the Follow button into FollowButtonRegion for the reorder.
2. Followers/Following rows now subscribe to kind-0 metadata (batched, capped)
   and observe each user's metadata flow so names + avatars render instead of
   raw npubs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:56 +03:00
nrobi144andClaude Opus 4.8 dd6b779463 test(desktop): DesktopMutualFeedFilter unit tests + profile parity manual sheet
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:56 +03:00
nrobi144andClaude Opus 4.8 4ab73789a6 feat(desktop): profile Zaps received tab
- Wire real PrivateZapCache(signer) into DesktopIAccount (was a null stub)
- Subscribe to kind 9735 receipts p-tagging the profile; aggregate sats per
  zapper (private zaps decrypt only on your own profile, else fall back to the
  public zap-request pubkey)
- New ProfileZapRow; tab header shows the total sats

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:56 +03:00
nrobi144andClaude Opus 4.8 0ab1930123 feat(desktop): profile header actions — Message, Add-to-list, Share
- Message opens the Messages column + 1:1 room via a DesktopDmRoute bridge
  (observed by both deck and single-pane layouts)
- Add to list: pack picker from LocalFollowPacksState, appends via
  FollowListEvent.add + broadcast
- Share copies a nostr: profile link to the clipboard
- Actions live in the existing writeable/other-profile overflow menu, so
  self/read-only are already excluded

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:56 +03:00
nrobi144andClaude Opus 4.8 bc482cb6eb feat(desktop): five profile tabs — Followers, Following, Relays, Bookmarks, Mutual
- Scrollable tab row; Followers/Following render UserSearchCard from live
  contact-list subscriptions; Relays render a new RelayRowCard from kind 10002;
  Bookmarks (kind 10003 → DesktopBookmarkFeedFilter) and Mutual (new
  DesktopMutualFeedFilter) render FeedNoteCard
- Respects the shared mute/block hidden-set via DesktopFeedViewModel

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:56 +03:00
nrobi144andClaude Opus 4.8 e862b3ecac feat(desktop): profile header polish — banner, rich-text bio, CLINK offer field
- Render kind-0 banner image in the profile card header
- Render bio as rich text (mentions/hashtags/links) via DesktopRichText
- Add CLINK offer (noffer) field to Edit Profile + shared EditProfileFields

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:55 +03:00
nrobi144andClaude Opus 4.8 b2b5bccec1 docs: desktop profile parity plan + per-phase sub-plans
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 09:39:55 +03:00
vitorpamplonaandgithub-actions[bot] b575f9078d chore: sync Crowdin translations and seed translator npub placeholders 2026-07-29 03:56:46 +00:00
Vitor PamplonaandGitHub 52a8cc338a Merge pull request #3793 from vitorpamplona/claude/highlight-events-browser-ywz5wr
Add NIP-84 highlights support: composer, feed, and share intent
2026-07-28 23:53:41 -04:00
Claude 7927e1de1b feat(highlights): rich comment field + source-event preview in composer
Address composer feedback:
- The annotation field is now the short-note composer's rich MessageField
  (@-mention + custom-emoji autocomplete, inline previews). On publish it becomes
  the highlight's `comment` tag, with the mentions (`p`), emoji, URLs (`r`),
  hashtags and quotes it references emitted as their own tags — via
  NewMessageTagger + a new tag-builder initializer on HighlightEvent.build().
- A nostr source now renders as a reply-style preview card (NoteCompose, quoted
  style) instead of showing a URL field; the URL field appears only for web
  sources. The source note is resolved from the a/e coordinate carried in the
  route.

Verified with :quartz:jvmTest and :amethyst:compileFdroidDebugKotlin.
2026-07-29 03:38:29 +00:00
Vitor Pamplona 1bda3ab31b v1.13.1 v1.13.1 2026-07-28 23:17:02 -04:00
Claude 55cc4ce9dc feat(highlights): highlight-a-note menu action; drop composer preview
- Add a "Highlight" action to the note action menu (3-dot menu + chat long-press
  sheet). It opens the NIP-84 composer pre-tagged with the note as source — an
  `a` reference for an addressable article, else an `e` reference, plus the
  author `p` — with the passage left for the user to type/paste. Prose kinds
  only (text notes, long-form), and never a private rumor (a public highlight
  would leak an e-tag of the unsigned rumor). This is the in-app entry point for
  "post with the source as an event".
- Remove the live preview from the New Highlight screen per review.
- Add two real-world regression tests for the shared-highlight parser: Chrome
  "copy link to highlight" with a prefix/suffix fragment (incl. an encoded
  hyphen inside the prefix) and a long start-only fragment with encoded commas.

Verified with :quartz:jvmTest and :amethyst:compileFdroidDebugKotlin.
2026-07-29 02:50:53 +00:00
Claude 155ef8e46c feat(highlights): richer composer UI and full NIP-84 source coverage
Redesign the New Highlight composer as a pull-quote you craft: a rounded tonal
hero card with a quotation-mark watermark, a highlighter-amber accent bar, and
the passage in large type, plus a live highlighter-pen preview (reusing the feed's
HighlightedQuote) and icon-led source/note fields.

Also extend HighlightEvent.create()/build() to emit a/e/p tags, so the builder
now covers every NIP-84 source — a web page (r), a nostr article (a), a nostr
note (e), each with optional author attribution (p) and context — not just web
highlights. Route.NewHighlight and the composer ViewModel carry the nostr source
(address/event/author) and context through so a future in-app "highlight this
passage" action can open the composer for a nostr article/note.

Covered by a new builder test for the a/e/p tags; verified with :quartz:jvmTest
and :amethyst:compileFdroidDebugKotlin.
2026-07-29 02:27:49 +00:00
Vitor PamplonaandClaude 9189955155 fix(quartz): drop the paren the URL trim orphans in the passage
`trimUrlEnd` strips a wrapping `)` off the URL token, but the `(` it opened
stayed at the tail of the passage, so sharing
`See this quote (https://example.com/article)` published a highlight whose
content read `See this quote (`.

Trim an unbalanced bracket left at the end of the passage after the URL token
is removed. Only an unbalanced one at the very end goes, so `He said (see
below) https://…` keeps its matched pair, and `.../Mercury_(planet)` — where
nothing was trimmed off the URL — is untouched on both sides.

The existing paren tests only asserted `result.url`, which is why this slipped
through; the new cases assert `result.quote` too, including a regression guard
for the slug-paren direction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 02:22:07 +00:00
Claude 5b07817cf8 feat(android): Highlights feed with top-nav filters and add button
Adds a browsable Highlights feed (NIP-84 kind:9802) modeled file-for-file on
the Git Repositories feed pattern, since highlights are announcement-like
regular events with the same follow-list top-nav filtering.

New feed stack under ui/screen/loggedIn/highlights:
- dal/HighlightsFeedFilter — AdditiveFeedFilter scanning LocalCache.notes for
  kind 9802 (regular events, so notes rather than addressables), gated by the
  selected top-nav follow list via FilterByListParams.
- datasource/ assembler + compose subscription + PerUserAndFollowList sub-
  assembler + makeHighlightsFilter dispatcher, with per-relay subassemblies for
  follows / authors / muted / global / hashtag / geohash. Communities aren't a
  highlight concept, so those sets fall through and aren't offered.
- HighlightsScreen (DisappearingScaffold + shared RenderFeedContentState; the
  existing RenderHighlight card handles rendering) with a HighlightsTopBar
  FeedFilterSpinner and a NewHighlightButton FAB into Route.NewHighlight.

Wiring into the shared plumbing mirrors gitRepositories exactly: Route.Highlights
+ AppNavigation, NavBarItem enum/catalog/drawer lists (FormatQuote icon, already
in the subset font), AccountFeedContentStates (feed state + update/delete/trim),
RelaySubscriptionsCoordinator, TopNavFilterState.highlightsRoutes,
AccountSettings.defaultHighlightsFollowList + changer, Account live follow-list
flows, LocalPreferences persistence, ScrollStateKeys, and the bottom-bar
preloader. Verified with :amethyst:compileFdroidDebugKotlin.
2026-07-29 02:22:07 +00:00
Claude 927cc9449c fix(quartz): keep balanced trailing parens in shared highlight URLs
The URL trailing-punctuation trim now leaves a closing bracket in place when
it is balanced within the token, so a shared Wikipedia link like
`.../wiki/Mercury_(planet)` keeps its `)` while a wrapping `(https://…)` still
loses the paren the surrounding prose added.
2026-07-29 02:22:06 +00:00
Claude 1f35339866 feat(android): share browser selection to a NIP-84 highlight composer
Adds a "New Highlight" share target and composer so a user can select text in
a browser, hit Share → Amethyst, and publish it as a kind:9802 highlight.

- Manifest: a text/plain-only ShareAsHighlightAlias activity-alias, matched at
  runtime by ShareIntentRouting.isShareAsHighlight (mirrors the Send-as-DM
  alias pattern).
- AppNavigation: both the launch-intent and warm onNewIntent parse blocks now
  branch on the highlight alias, run the shared text through
  SharedHighlightParser, and open Route.NewHighlight pre-split into
  quote/url/prefix/suffix.
- NewHighlightScreen + NewHighlightPostViewModel: a trimmed composer (passage,
  source URL, optional note — no polls/zaps/media/scheduling) that publishes via
  account.signAndComputeBroadcast(HighlightEvent.build(...)).

Also adds HighlightEvent.build(), the unsigned EventTemplate counterpart of
create(), for the sign-and-broadcast pipeline. Verified with :quartz:jvmTest
and :amethyst:compileFdroidDebugKotlin.
2026-07-29 02:22:06 +00:00
Claude 69c3e3accc feat(quartz): parse browser shares into NIP-84 highlights
Adds a shared-highlight parsing layer under nip84Highlights/parse that turns
the free-form text a browser hands Amethyst on "Share selection" into the
pieces of a kind:9802 highlight:

- SharedHighlightParser normalises selection-only, selection+URL, URL-only and
  "link to highlight" (#:~:text= fragment) shares into a SharedHighlight.
- TextFragmentParser decodes/strips WICG text-fragment directives (prefix,
  start, end, suffix), leaving literal '+' verbatim.
- UrlTrackerCleaner strips utm_*/fbclid/etc. from the source URL per NIP-84's
  "clean the URL from trackers" guidance, preserving path and fragment.

Also adds a HighlightEvent.create() builder overload that assembles the r,
textquoteselector, context and comment tags from parsed data, centralising
what the desktop publish action does by hand.

Covered by commonTest suites for each piece plus a builder round-trip.
2026-07-29 02:22:06 +00:00
Vitor PamplonaandGitHub 87b83a7c16 Merge pull request #3792 from vitorpamplona/claude/highlight-author-attribution-25e4nb
Fix highlight author detection and improve source display
2026-07-28 22:21:02 -04:00
Vitor PamplonaandGitHub 551add4be0 Merge pull request #3791 from vitorpamplona/claude/highlighted-text-richtext-parser-88ghgv
Pass comment tags to DisplayHighlight for proper rendering
2026-07-28 22:16:53 -04:00
Claude 3eb8914c3e fix(highlights): attribute to the author-marked p tag, drop alt captions
A kind:9802 highlight that mentions users before its author rendered the
wrong name and a stray "published by …" caption.

- HighlightEvent.author() took the first p tag regardless of role, so a
  highlight with leading "mention" p tags was attributed to a mention
  instead of the "author"-marked one. Prefer the NIP-84 author marker,
  falling back to the first p tag for highlights (including Amethyst's own)
  that omit the marker.

- DisplayEntryForNote used the source note's title/subject/alt as a caption.
  For a kind-1 note there is no title/subject, so it fell to the NIP-31 alt
  tag — which clients like Jumble fill with a generic "This event was
  published by https://jumble.imwald.eu." line. Drop alt from the lookup and
  name the source by its event kind instead, kept clickable to the note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJh3QYonEa9hw6mJcnrmve
2026-07-29 02:07:11 +00:00
Claude 74be87c305 fix: render custom emoji in highlight comments
The NIP-84 highlight comment was passed to TranslatableRichTextViewer with
an empty tag list, so custom emoji (:shortcode:) never resolved against the
event's `emoji` tags and rendered as raw text.

Thread the highlight event's tags through to the comment's rich-text viewer
so the emoji map is built from them, matching how regular text notes render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyyodHWxSHeiY6pndPxGSb
2026-07-29 02:04:05 +00:00
Vitor PamplonaandGitHub ebf04de516 Merge pull request #3790 from vitorpamplona/claude/home-screen-event-toggles-q75xwc
Add per-content-type toggles for Home feed event kinds
2026-07-28 20:55:56 -04:00
Vitor PamplonaandGitHub 79e70e1161 Merge pull request #3789 from vitorpamplona/claude/buzz-media-upload-decode-gc5hxm
Add BUD-01 read-auth retry for gated Blossom blob downloads
2026-07-28 20:40:00 -04:00
Vitor PamplonaandGitHub 3411f75c4a Merge pull request #3788 from vitorpamplona/claude/nip29-wisp-relay-test-dloy9s
Support NIP-29 group relays in first-party AUTH logic
2026-07-28 20:38:50 -04:00
Claude defb898987 refactor: use Hex.isHex64 instead of a regex for the blob-id check
blossomHashOrNull runs on every media URL the feed loads. Quartz's unrolled
Hex.isHex64 is the fast path for validating a 32-byte hex id (~30% faster
than isHex, far faster than a regex match). It only checks the first 64
chars and doesn't verify length, so keep an explicit length == 64 guard to
reject longer segments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao9w26c2gAm4gjJdhgvLyp
2026-07-29 00:35:53 +00:00
Claude 7d1c45607b perf: preemptively sign Blossom reads for known auth-gated hosts
The retry-on-401 interceptor re-probed anonymously on every blob, so each
image from an auth-gated host (e.g. a Buzz community feed, where nearly all
media is on one host) paid a wasted 401 round trip before the signed retry.

Remember hosts that answered 401 and attach the cached token up front on
their subsequent blobs — one round trip instead of two in steady state. The
first blob per host still costs the probe; the learned set falls back to an
anonymous request when no signer is available so a logged-out user never
loops.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao9w26c2gAm4gjJdhgvLyp
2026-07-28 23:54:31 +00:00
Vitor Pamplona bcfe2745f2 Merge remote-tracking branch 'upstream/main' 2026-07-28 19:50:55 -04:00
Vitor PamplonaandGitHub 723c90c0d7 Merge pull request #3787 from vitorpamplona/claude/geohash-single-level-precision-gise2y
Add CONTINENT geohash level for whole-globe location channels
2026-07-28 19:47:52 -04:00
Vitor PamplonaandClaude Opus 5 096fd0833a Merge PR: fix(desktop): cache OS Keyring handle so startup only prompts once
Merges nostr proposal ed443f87 into main:
- Cache the Keyring behind double-checked locking so the OS backend is
  opened once per SecureKeyStorage rather than per save/get/delete. Cold
  start touches storage at least twice (metadata AES key, then the active
  account nsec), which surfaced as two keychain unlock prompts.
- Add a KeyringHandle seam so the test suite can substitute an in-memory
  backend instead of touching the real OS keychain, plus tests pinning the
  single-open invariant, including a 16-thread concurrent first-touch race.

Sharing one handle across Dispatchers.IO threads is safe on all three
backends: the macOS path resolves to pt.davidafsilva.apple.OSXKeychain,
itself a static singleton with no mutable instance state, so every
Keyring.create() already shared it; the Freedesktop and Windows backends
hold a single final collaborator assigned in their constructor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:46:46 -04:00
Vitor PamplonaandClaude Opus 5 1a82c9b9c8 Merge PR: fix(desktop): reload the visible page when switching accounts
Merges nostr proposal 22a8247d into main:
- Wrap MainContent in key(account.pubKeyHex) so the account-scoped subtree
  is torn down on an account switch. Deck layout and workspace state are
  remembered outside this key and survive.

Fixes a stale-identity leak, not just staleness: NotificationsScreen holds
its accumulated items in an unkeyed `remember { EventCollectionState(...) }`,
so account A's notifications stayed on screen under account B even though
the subscription below re-keyed correctly. ReadsScreen has the same unkeyed
collection, and DeckColumnContainer's ColumnNavigationState is keyed on
column.id only, so each column's screen stack also survived the switch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:45:57 -04:00
Claude 7d1caf590a fix: authenticate to NIP-29 host relays of joined groups so private group content loads
A private/closed NIP-29 group serves its content (kind-9 chat, kind-11
threads, …) only over a NIP-42-authenticated connection. Amethyst's
group-content reads are all `#h`-scoped with all authors, so they never
name the user's pubkey, and a group host relay (e.g. wss://chat.wisp.talk)
is not in any of the NIP-65/DM/search/… lists that feed
`account.trustedRelays`. The per-account first-party AUTH gate therefore
returned false and Amethyst never AUTHed, so the relay refused the content
with `auth-required` — the group's public 39000 metadata still loaded, so
the group appeared but showed no messages.

Treat a NIP-29 relay group the user explicitly joined (their kind-10009
list) as a first-party reason to authenticate with its host relay,
mirroring the existing `BuzzWorkspaces.isJoined` carve-out. Reads of a
group the user has not joined (e.g. browsing a relay's public directory)
still do not AUTH.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018puT2YnevbLHYG2PdCLAeh
2026-07-28 23:45:49 +00:00
Claude 493aed8609 feat: sign Blossom read-auth to view media on auth-gated hosts
Buzz's private media relay (*.communities.buzz.xyz) gates blob downloads
behind BUD-01 read auth, returning `401 {"error":"authentication failed"}`
to anonymous GETs. Amethyst loaded every media URL anonymously through
Coil/OkHttp, so those images (and their thumbnails) never decoded.

Quartz already had BlossomAuthorizationEvent.createGetAuth but nothing in
the app ever called it. Wire it into the media HTTP client:

- BlossomReadAuthInterceptor: on a 401 for a GET whose URL last segment is
  a Blossom sha256 filename (covers `<hash>.png` and `<hash>.thumb.jpg`),
  retry once with a signed `Authorization: Nostr <event>` header. Narrowly
  gated so unrelated 401s never trigger a second request or any signing.
- BlossomReadAuthTokenProvider: bridges the suspend signer synchronously
  (runBlocking + timeout so a slow remote/external signer can't pin the
  OkHttp thread) and caches one server-scoped token per host, which also
  covers derived blobs like thumbnails.
- BlossomAuth.createGetAuth exposes the read-auth builder to the app layer.

Public Blossom/NIP-96 hosts stay zero-overhead: they answer 200, so no
token is ever signed for them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao9w26c2gAm4gjJdhgvLyp
2026-07-28 23:25:57 +00:00