Commit Graph
17885 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub 011d5f2fa5 Merge pull request #3757 from vitorpamplona/claude/nip84-highlight-marker
feat(highlights): render NIP-84 quotes with a highlighter-pen marker
2026-07-27 22:48:01 -04:00
Vitor PamplonaandClaude Opus 5 7d13f373ac feat(highlights): keep auto-translation on the marked quote
Replacing the markdown pipeline dropped TranslatableRichTextViewer, and
with it the ML Kit auto-translation of the quoted passage. Restore it
without giving up the marker.

A translation rewrites the passage, so the character offsets that locate
the quote inside its context stop pointing at anything. Translate the
quote as well and re-find it in the translated context: ML Kit works
sentence by sentence, so a quote that is one or more whole sentences --
the common case, since people highlight sentences -- comes back identical
whether it is translated alone or inside its paragraph.

  untranslated             -> context, original span marked
  translated + quote found -> translated context, translated span marked
  translated + not found   -> translated quote alone, fully marked

The fallback is free: HighlightQuote.of already returns the quote alone
when the needle is not in the haystack, so the not-found case needs no
special casing. Marking a guessed span, or claiming the whole paragraph
was highlighted, would both be worse than showing less.

That second translation must not draw its own status bar, so add
rememberTranslation() to both flavors: play reuses the existing
translateAndCache and its cache; fdroid, which ships no translation
service, returns the content unchanged.

Also indent the "Auto-translated from X to Y" line to the quote's own
15dp so it lines up with the text rather than the bar.

Verified on device: an English highlight renders as a fully translated
Portuguese paragraph with the quoted sentence marked inside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:39:57 -04:00
Vitor PamplonaandClaude Opus 5 057623e3ab feat(highlights): render NIP-84 quotes with a highlighter-pen marker
The renderer never drew a highlight. It synthesised a markdown string --
blockquote each line with "> ", wrap the quoted span in "**" -- and handed
it to the rich-text viewer, so a highlight arrived as bold text. That also
meant the quoted article prose was parsed as markdown, so any *, _, # or [
in it was interpreted as formatting rather than shown.

Drop the markdown round-trip and paint the marker behind the glyphs. The
stroke is drawn per visual line from the TextLayoutResult, so it follows
soft wraps and stops at real glyph edges. Per-line rounded rects rather
than SpanStyle(background), which can only ever be a hard full-line-height
rectangle -- that is what buys the rounded pen ends.

Size the stroke from the baseline and font size, not the line box, so
leading and stroke weight stay independent knobs.

Along the way:

- Locate the quote as an index range instead of context.replace(), which
  marked every occurrence when a quote repeated. Use the W3C
  TextQuoteSelector prefix -- already on the event, previously ignored --
  to disambiguate.
- Restore 1.35em leading. The markdown path forced 1.5em via
  MarkdownTextStyle; the ambient bodyLarge sets no lineHeight at all, so
  rendering plain text inherited the font's intrinsic ~1.2em.
- Indent the source attribution by the quote's own 15.dp so it lines up
  with the text rather than the bar, and space the comment, quote and
  attribution 8.dp apart -- they were flush at 0.dp.
- Clamp the stroke to the column so it cannot be clipped on full-width
  lines.

Light keeps a near-opaque yellow with dark glyphs reading through it. Dark
cannot do that, so it gets a translucent amber that glows rather than
covers. Not derived from the user's accent: a highlighter reads as yellow.

The quoted passage no longer routes through TranslatableRichTextViewer, so
it loses its auto-translate affordance; drawing the marker requires owning
the text layout. The author's own comment above the quote keeps it.

Verified on device in both themes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:03:28 -04:00
Vitor PamplonaandGitHub 329bfe95e7 Merge pull request #3752 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 21:40:02 -04:00
vitorpamplonaandgithub-actions[bot] 99f0443a98 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-28 01:37:22 +00:00
Vitor PamplonaandGitHub 9f7056165d Merge pull request #3755 from vitorpamplona/claude/bottom-nav-per-user-s5h6p4
Move bottom-bar persistence from app-global to per-account synced settings
2026-07-27 21:34:48 -04:00
Vitor PamplonaandGitHub 918cec6bf8 Merge pull request #3756 from vitorpamplona/claude/notification-settings-account-display-oz5ujd
Display user profiles in notification settings account list
2026-07-27 21:34:26 -04:00
Vitor PamplonaandGitHub d62a94f2eb Merge pull request #3753 from vitorpamplona/claude/changelog-1.13-audit
docs(changelog): tighten v1.13.0 highlights and audit against commits since v1.12.6
2026-07-27 20:02:46 -04:00
Claude 586346a68f fix: keep bottom-bar edits ordered to prevent a stale revert
Persisting a bottom-bar edit went through `launchSigner { account.change... }`,
which dispatches on a multi-threaded pool. Because the reactive StateFlow emit
happened inside that coroutine, two quick edits (e.g. tapping Add on several
catalog rows) could complete out of order: the older list would win the flow,
and the settings screen — which re-seeds its editable list from the flow via
`LaunchedEffect(savedItems) { syncFrom(...) }` — would visibly revert the newer
edit and publish the stale list in the NIP-78 event.

Apply the change to the in-memory flow synchronously on the caller (UI) thread
via `Account.applyBottomBarItems` (a non-suspending emit + local save, both
non-blocking) so rapid edits stay strictly ordered, and run only the
sign/encrypt/publish off-thread. Restores the ordering guarantee the previous
synchronous `tryEmit` had.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJiPHArXZ7P5EvZa7GN9fP
2026-07-27 23:58:13 +00:00
Claude 39c05fb538 refactor: use shared UserPicture/UsernameDisplay in notification settings
Replace the hand-rolled RobohashFallbackAsyncImage + observeUserInfo +
CreateTextWithEmoji in the background-accounts list with the app's standard
user components: LoadUser resolves the User behind each npub, and
ClickableUserPicture / UsernameDisplay render the picture and name. This
reuses the shared metadata observation, robohash fallback, custom-emoji
rendering, per-user nickname handling, and npub fallback instead of
duplicating that logic here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvB5t1ittHpw4PdJQYZYfp
2026-07-27 23:55:13 +00:00
Claude 7a41e21272 feat: make bottom navigation bar per-account via NIP-78
The bottom nav row configuration was an app-global setting stored in the
shared DataStore, so every account shared one bar. Move it into the
per-user NIP-78 app-specific data event (AppSpecificDataEvent) so each
account keeps its own bar and it syncs across the user's devices.

- Add `navigation.bottomBarItems` to AccountSyncedSettingsInternal (the
  serialized/encrypted synced-settings blob) and mirror it as a StateFlow
  in AccountSyncedSettings (seed / toInternal / updateFrom).
- Add AccountSettings.changeBottomBarItems, Account.changeBottomBarItems
  (republishes the NIP-78 event), and AccountViewModel.changeBottomBarItems
  / bottomBarItemsFlow().
- Remove bottomBarItems from the app-global UiSettings / UiSettingsFlow /
  UiSharedPreferences (including the now-unused encode/decode migration
  helpers).
- Point the live bar, navigation rail, preloaders, subscriptions and the
  Bottom Bar settings screen at the per-account flow.

No migration from the previous app-global setting: accounts start from the
default bar, matching the requested behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJiPHArXZ7P5EvZa7GN9fP
2026-07-27 23:30:17 +00:00
Vitor PamplonaandGitHub a9b22a927f Merge pull request #3754 from vitorpamplona/claude/finduserstest-metadata-failure-24a9ck
Fix flaky FindUsersTest by retaining users in cache
2026-07-27 19:28:27 -04:00
Claude 5be70e3a86 feat: show account picture and name in notification settings
The background-accounts list in the Notification Settings screen showed
each account by its npub only. Resolve the User behind each account and
observe its live metadata so the row displays the profile picture and
best display name (with the npub as a secondary line), falling back to
the short npub while metadata loads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvB5t1ittHpw4PdJQYZYfp
2026-07-27 23:23:20 +00:00
Vitor Pamplona 70646f4609 adds gigi to changelog 2026-07-27 19:05:55 -04:00
Vitor Pamplona 8a587da45a updates dependencies 2026-07-27 19:05:45 -04:00
Claude 2d0ccb4d70 fix: stop FindUsersTest flaking when GC evicts weakly-cached users
DesktopLocalCache stores users in LargeSoftCache, which holds each User
via a WeakReference. FindUsersTest populated the cache but kept no strong
reference to the created users, so a GC between consumeMetadata and
findUsersStartingWith could collect them — LargeSoftCache.forEach then
skips (and evicts) the cleared entries, making the search return fewer
results. multipleUsersWithMetadata allocates the most objects and tripped
this most often (AssertionError at FindUsersTest.kt:115).

Verified with a forced-GC probe: without a strong reference all three
users were evicted (found=0); holding a reference kept all three (found=3).

Pin each test's users in a strong-reference list that stays reachable
through the assertions, mirroring how Notes/Account/follow lists keep
authors alive in the real app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X4BThbs8tz9egmFDYv2RPB
2026-07-27 23:04:04 +00:00
Vitor PamplonaandClaude Opus 5 f55b3cb6d9 docs(changelog): tighten v1.13.0 highlights and audit against commits
Highlights carried full feature descriptions that the sections below
already repeated. Reduce each to a single line and fold the detail down
into the section that owns it.

Audit the notes against all 1961 non-merge commits since v1.12.6:

- Drop the claim that WebSocket frame dispatch moved to a dedicated pool.
  It landed in a5c2a8b2c1's parent and was reverted 23 minutes later
  because the pool regressed. Replace it with the two receive-path wins
  that did ship (CachingEventDecoder, ParallelEventVerifier).
- Add the chat redesign, which had no section at all: bubbles,
  swipe-to-reply, name colors, jumbo emoji, day headers, the two-stage
  long-press sheet, and the new-conversation chooser.
- Add in-app podcast authoring, which the Podcasts section omitted in
  favour of consumption only.
- Promote the resource-usage ledger out of a single Wallet line into its
  own section, alongside the background-service master switch and
  memory-pressure trimming.
- Add large-screen support, NIP-85 nicknames, Birdstar and PS1 cards,
  compose signature, Marmot group icons, and other unreferenced work.

Remove the Upgrading section. No prior release has one, and all three of
its items were consequences of features documented further down, so they
now sit with the feature that causes them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:03:09 -04:00
Vitor PamplonaandGitHub 2beb75b59e Merge pull request #3751 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 18:23:37 -04:00
vitorpamplonaandgithub-actions[bot] a4ec34a96d chore: sync Crowdin translations and seed translator npub placeholders 2026-07-27 21:13:25 +00:00
Vitor PamplonaandGitHub 36792d09b1 Merge pull request #3750 from vitorpamplona/claude/update-changelog-3mwv4t
docs: update v1.13.00 changelog with new features
2026-07-27 17:10:10 -04:00
Claude 9df51641ea docs(changelog): split v1.13.0 fold-in notes into shorter sentences
Break the newly added BOLT12, Buzz Agent Work board, Blossom, Cashu,
NWC, search-indexing, and geode bullets into short verb-first sentences,
removing em-dash run-ons to match the changelog house style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hmbpUJPxrsN4m85Kwemcw
2026-07-27 21:06:24 +00:00
Claude 6c1412b7c9 docs(changelog): fold post-2026-07-24 work into v1.13.0 notes
Adds the BOLT12 payments & zaps (NIP-B1) work, the Buzz Agent Work
board / workflow runner, Concord message editing, the Blossom gallery
+ importer, Cashu P2PK-token claiming, NWC NIP-44/deep-link/notify
improvements, wider NIP-50 search indexing, and the geode release
pipeline + pluggable event store, plus assorted fixes. Credits dergigi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hmbpUJPxrsN4m85Kwemcw
2026-07-27 20:55:39 +00:00
Vitor PamplonaandGitHub 37759cf517 Merge pull request #3744 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 16:45:55 -04:00
vitorpamplonaandgithub-actions[bot] 1c581eb07a chore: sync Crowdin translations and seed translator npub placeholders 2026-07-27 20:22:29 +00:00
Vitor PamplonaandGitHub 0b73236e76 Merge pull request #3749 from vitorpamplona/claude/buzz-event-p-tags-7ud8bj
Resolve @mentions in message bodies to p-tags across all chat types
2026-07-27 16:19:46 -04:00
Claude 89add9d053 refactor(chat): require an explicit Dao on NewMessageTagger
Drop the `dao = LocalCache` default; the two callers without an
AccountViewModel now pass `dao = LocalCache` explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 20:17:07 +00:00
Claude 8283b22c2f refactor(chat): let LocalCache implement Dao directly
LocalCache already has getOrCreateUser/getOrCreateNote/getOrCreateAddressableNote,
so make it implement Dao and default NewMessageTagger's `dao` to LocalCache
itself — no wrapper object, no interface-default methods. Dao drops the
`suspend` modifiers (LocalCache's lookups are synchronous) so the object
satisfies the interface; AccountViewModel's delegating overrides follow suit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 20:13:05 +00:00
Claude 3562a62e0a refactor(chat): keep Dao abstract, implement LocalCache lookups in impls
Move the LocalCache calls off the Dao interface (back to a pure abstract
contract) and into the implementations: AccountViewModel keeps its own
overrides, and the default Dao used by callers without a ViewModel is an
anonymous implementation on NewMessageTagger's `dao` parameter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 20:04:25 +00:00
Claude b4da12300e refactor(chat): fold LocalCache Dao defaults into the Dao interface
Replace the standalone LocalCacheDao object with default method
implementations on the Dao interface itself, backed by LocalCache. The
NewMessageTagger `dao` parameter now defaults to a bare `object : Dao {}`,
so callers with no AccountViewModel (model-layer sends, the notification
receiver) just omit it, while UI callers keep passing their AccountViewModel
(which resolves to the same LocalCache calls).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 19:57:09 +00:00
Claude d305c03e54 feat(chat): resolve mentions in minichat and notification quick-replies
The lightweight reply paths built their events from raw text, so a person
cited with `@name`/`nostr:` was neither resolved into a `nostr:` reference
nor tagged with `p`. Run NewMessageTagger on these paths too, backed by a
new LocalCache-based Dao so they work without an AccountViewModel:

- New `LocalCacheDao`: a `Dao` delegating to `LocalCache`, for mention
  resolution outside a ViewModel (model-layer sends, background receivers).
- `Account.sendMinichatReply` (kinds 9 + 1111): resolve mentions and emit
  `p` tags for cited users across the public-chat, Buzz, and NIP-29 group
  branches (parent author excluded to avoid a duplicate).
- `NotificationReplyReceiver.sendPublicReply` (kinds 1 + 1111): same
  resolution for the system notification quick-reply.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 19:46:00 +00:00
Claude 11147aac10 fix(chat): add p tags for people cited in kind 9/11/1111 messages
Extends the mention p-tag fix to the shared chat/thread composers, which
resolved `@`/`nostr:` mentions via NewMessageTagger but then dropped the
cited users from the signed event:

- kind 9 (ChatEvent, NIP-29 group chat): the reply path tagged only the
  parent author and the plain build path tagged no one. Emit `p` tags for
  every cited body user (reply path keeps the parent's relay-hinted tag and
  excludes it from the extra set to avoid a duplicate).
- kind 1111 (CommentEvent) minichat reply: replyBuilder auto-tags the
  parent/root author but body mentions were dropped. Notify the other
  cited users (parent excluded to avoid a duplicate).
- kind 11 (ThreadEvent, NIP-29 group thread): the ShortNote thread branch
  added no `p` tags at all; emit them from the cited body users, matching
  the sibling Poll/ZapPoll branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 19:36:57 +00:00
Vitor PamplonaandGitHub 4179a6312a Merge pull request #3747 from vitorpamplona/claude/quartz-negentropy-stalls-6g9ukr
Fix NIP-77 negentropy stall against relays that refuse via NOTICE
2026-07-27 15:28:33 -04:00
Claude 9de8eae5fd test: broad live NIP-77 validation across 30 public relays
Adds NegentropyMultiRelayLiveTest (gated NEG_MULTI=1): runs negentropySyncOrFetch
against 30 reachable public relays and asserts none hangs. Live run: 0 hangs,
0 errors — 10 reconciled via native negentropy, 20 fell over to paging, across
9+ relay softwares (strfry, ditto, purplepag.es, nostr.wine, nostr-rs-relay,
NFDB, rockstr, wot-relay, nostrcheck).

Confirms both fallback paths: the NOTICE fast-path (~1-4s) for relays whose
refusal names negentropy / unknown-envelope, and the idle-watchdog backstop
(~20s) for the rest (e.g. damus silently ignores NEG-OPEN, snort answers
"Unknown message type: NEG-OPEN") — now reliable because NOTICE/CLOSED no longer
reset the watchdog. Matrix recorded in the plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6ZVTixuc1ef8eGB6MQHRn
2026-07-27 19:23:46 +00:00
Vitor PamplonaandGitHub b5e5d0c4e7 Merge pull request #3748 from vitorpamplona/claude/buzz-amethyst-support-nfxog7
Add Buzz job/workflow boards and CLI agent/workflow commands
2026-07-27 15:20:16 -04:00
Claude 30b9e589d4 fix: harden negentropy window-splitting and error classification
Audit follow-ups on the NIP-77 client, found while reviewing the notice-rejection fix:

- isOverflow was too broad. A bare "too many"/"too large" match meant a
  NON-shrinking error ("too many requests", "too many concurrent subscriptions")
  was read as a set-too-large overflow. Such an error doesn't shrink with the
  window, so every created_at split re-triggers it and reconcileWindows walks
  toward 1-second leaves, queueing up to ~2^31 Filters (OOM + relay hammering).
  Tightened to result-set-qualified phrases (too many records / too many query
  results / result set too large / max_sync_events); rate/quota errors now fail
  over to paging.

- Added a MAX_WINDOWS (100k) backstop in reconcileWindows: a wording-independent
  guard that bails to paging if a split ever fails to converge, so no novel
  overflow-looking-but-non-shrinking error can storm.

- Window split dropped future-dated events. On overflow the upper child was
  copy(until = hi) with hi = until ?: now(), so once any split happened, events
  with created_at > now() (clock skew) were excluded though the un-split path
  included them. The upper child now keeps the window's original until (may be
  null = unbounded); the split math still uses now() so it converges.

- Hardened the NOTICE rejection matcher. isNegentropyRejectionNotice matched
  bare "envelope"/"NEG-OPEN"/"NEG-MSG"; since a NOTICE has no subId and every
  connection listener sees it, an unrelated notice on a shared connection could
  abort a healthy reconcile mid-handshake. Narrowed to "negentropy"/"unknown
  envelope"; the now-un-defeated idle watchdog is the wording-independent backstop.

- NegentropyStoreSync up-direction memory: haveBatches was an UNLIMITED channel
  drained by a single network-bound uploader, so a first push of a large store
  buffered O(local-set) ids. Bounded it like needBatches to back-pressure the
  reconcile.

- Docs: flagged negentropySyncOrFetch's O(delivered) cross-phase dedup memory
  (steer bulk mirrors to negentropySync/negentropyReconcile); corrected the
  stale onEvent "reader thread" note (it runs on the delivery consumer).

Tests: NegentropyErrorClassificationTest pins both wording classifiers;
NegentropyRejectionFallbackTest adds a rate-limit NEG-ERR case asserting paging
after exactly one NEG-OPEN per phase (no split storm).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6ZVTixuc1ef8eGB6MQHRn
2026-07-27 19:13:14 +00:00
Claude 1d44563d37 fix(buzz): add p tags for people cited in messages
The Buzz composers dropped `p` mention tags for people named in a
message body, so a member cited with `@name` was neither notified nor
linkable and the relay couldn't resolve the `nostr:` reference:

- Stream chat (kind 40002): only the reply-parent author got a `p` tag;
  body mentions from the tagger were ignored. Emit `p` tags for every
  cited user (dedup covers the reply author).
- Stream edit (kind 40003): carried no mentions at all — a mention added
  by an edit lost its `p` tag. Emit them from the edited text.
- Forum reply (kind 45003): never ran NewMessageTagger, so `@`-mentions
  stayed literal and only the reply-parent author was tagged. Run the
  tagger to resolve mentions and emit their `p` tags.
- Forum post (kind 45001): the root composer built the event from raw
  text with no mentions. Run the tagger there too.

Mirrors the mention p-tag handling every other chat kind already does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 19:09:30 +00:00
Claude 264866bcbb fix: detect NIP-77 negentropy refusal sent as a connection-level NOTICE
Relays that advertise NIP-77 in NIP-11 but refuse it at runtime answer a
NEG-OPEN with a connection-level NOTICE (which carries no subId) instead of a
subId-addressed NEG-ERR:

  - strfry with negentropy disabled: "ERROR: bad msg: negentropy disabled"
  - purplepag.es (no NEG envelope):  "failed to parse envelope: unknown envelope label"

reconcileStreaming only routed NegMsg/NegErr for its exact subId into the driver
channel, so the NOTICE was dropped and the driver blocked in receiveWithinIdle
with no terminating frame. Worse, the connection-level idle watchdog was bumped
by every relay message, so unrelated refusal chatter (a rejected keep-alive REQ
being re-CLOSED on re-sync) reset it forever and it never fired. Net effect:
negentropySync/negentropyReconcile hung against relay.primal.net and
purplepag.es, and negentropySyncOrFetch never reached its paging fallback.

Fix, in reconcileStreaming's connection listener:
  - route a CLOSED for our NEG subId into the driver as a terminal failure;
  - treat a negentropy-refusal NOTICE as terminal, bound to this session by
    phase (before the first valid NEG frame) + wording (isNegentropyRejectionNotice),
    so an unrelated NOTICE on a healthy relay mid-reconcile cannot abort a
    progressing sync;
  - stop bumping the idle clock on NOTICE/CLOSED so refusal chatter can no longer
    keep a dead sync alive; it now advances only on real progress (this session's
    NEG frames and the download REQs' events/EOSEs).

The refusal now surfaces as NegentropySyncException(UNAVAILABLE); negentropySync
throws promptly and negentropySyncOrFetch pages the same filter (both relays
answer ordinary REQs fine). Verified live: primal 0-event hang -> pages 11.7k
events in 6.4s; purplepag.es 0-event hang -> pages continuously.

Tests:
  - NegentropyRejectionFallbackTest: offline, deterministic; a scripted fake
    relay answers NEG-OPEN with each observed NOTICE and asserts the sync throws
    UNAVAILABLE fast and negentropySyncOrFetch sets pagedFallback.
  - NegentropyStallRepro: gated live repro against the three real relays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6ZVTixuc1ef8eGB6MQHRn
2026-07-27 18:12:01 +00:00
Claude b30d39e627 Merge remote-tracking branch 'origin/main' into claude/buzz-amethyst-support-nfxog7
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
2026-07-27 17:58:02 +00:00
Vitor PamplonaandGitHub b5ad53f668 Merge pull request #3746 from vitorpamplona/claude/health-connect-activity-consolidation-y8ao9e
Merge close workout sessions to reduce noise in Health Connect suggestions
2026-07-27 13:50:40 -04:00
Vitor PamplonaandGitHub 855435fcc1 Merge pull request #3742 from vitorpamplona/fix/buzz-create-channel
fix(buzz): make channel create, add-member and role changes actually work
2026-07-27 13:16:32 -04:00
Vitor PamplonaandClaude Opus 5 07abf5e537 fix(buzz): stream group state instead of refetching it
The previous two commits refetched a group's 39000-39003 whenever the relay
narrated a change, on the premise that Buzz could not stream them at all: it
signs them with `d`/`p` tags and no `h`, so an `#h` filter looked unable to match
and a filter without `#h` registers as a global subscription, which never
receives a channel-scoped event.

The first half of that was wrong. `filter_match_one` has an explicit fallback:
for an `#h` filter, when the event carries no `h` tag at all, it matches against
the stored `channel_id` — and these are stored channel-scoped. So an `#h` filter
both indexes the subscription under the channel (which is what makes it eligible
for the channel fan-out) and matches the events when they arrive.

So subscribe, like the rest of the app does. The per-channel `#h` subscription
that already keeps each joined group's chat live now carries its state kinds too,
and every screen updates through the flows it already observes. The fetch, the
event-bundle hook that triggered it and the cache lookup it needed are all gone.

Buzz-only: on a relay29-family relay these events are addressable with no
`channel_id` behind them, so an `#h` filter matches nothing there and the `#d`
directory filters keep serving them.

Verified on emulator-5554 with no fetch code left in the tree: promoting shows
the `admin` badge on the open members screen within seconds, and removing the
role clears it again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:47:06 -04:00
Vitor PamplonaandClaude Opus 5 aeaacfb1e5 refactor(buzz): make the group-state refresh app-wide, not one screen's job
The previous commit hung the refresh off the members screen, so only that screen
recovered from a stale roster: a rename, a visibility flip or a join seen from the
chat, the channel list or a Messages row stayed wrong until the next cold start.
The rest of the app is reactive — relay to LocalCache to screen — and this should
be too.

Move it into AccountViewModel's always-on event collector: any kind-40099 that
lands names its channel, so re-read that group's 39000-39003 into LocalCache and
every screen observing the group updates through the flows it already has. One
rule, no screen has to know about it.

Two things this had to work around:

- The event names its channel but not its host, and a note's relay list can still
  be empty when the bundle fires. LocalCache.relayGroupChannelsWithId resolves the
  host from the channels already in cache.
- Invalidating the standing state subscription does nothing: its filters are
  unchanged, so no new REQ goes out and no events come back. Measured — the badge
  did not move. It takes an explicit fetch, which is what
  Account.refreshRelayGroupState now does by group id.

Verified on emulator-5554: removing a role updates the badge on the open members
screen with no restart and no screen-local refresh code left in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:09:37 -04:00
Vitor PamplonaandGitHub e9074aa2cb Merge pull request #3745 from vitorpamplona/claude/event-searchable-review-swvvqu
feat(quartz): index the public petname on contact cards (30382)
2026-07-27 12:08:29 -04:00
Claude 4c42648c35 feat(quartz): index the public petname on contact cards (30382)
ContactCardEvent.indexableContent() already indexed the public summary
and topics; add the public petName() tag alongside them so a card can be
found by the nickname its author gave the target user. petName()/summary()
read the public tag array, so any petname kept in the NIP-44 encrypted
content stays out of the index (privacy preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHSTACrFHaRX1o6C5cEk6N
2026-07-27 15:59:54 +00:00
Vitor PamplonaandGitHub cd100e4272 Merge pull request #3743 from vitorpamplona/claude/event-searchable-review-swvvqu
Add SearchableEvent implementation to 18 event types
2026-07-27 11:55:27 -04:00
Vitor Pamplona b1d082159d Merge remote-tracking branch 'upstream/main' into fix/buzz-create-channel 2026-07-27 11:41:51 -04:00
Claude 8e79abaa82 feat(cli): fewer steps to connect a Buzz agent — accept-from-channel, agent up, doctor
Collapses the operator setup from an 8-flag command + two hand-written scripts to
essentially two commands, without removing any of the safety.

- `buzz workflow run` gains `--accept-from-channel` (parity with the job
  scheduler): scope intake to the channel's kind-39002 roster instead of pasting
  every teammate key. `--worktree` now defaults to the current directory.
- Ship the gated reference wrappers (tools/buzz-agent/workflow-agent.sh →
  agent+commit; workflow-ship.sh → push+PR after the gate), split around the
  approval gate the way agent-exec.sh is the one-shot ungated version.
- `buzz agent up RELAY --repo DIR --approver NPUB` — one command: resolves the
  channel (the relay's only one, or --channel), defaults worktree/intake, extracts
  the bundled wrappers to ~/.amy/buzz-agent, and delegates to `workflow run`. The
  only thing it can't default is the human approver.
- `buzz agent doctor [--repo DIR]` — preflight that turns the security checklist
  into a green/red report: gh authenticated, token can write to the repo, default
  branch protected against force-push, worktree clean. Exits non-zero if not.
- cli build: set duplicatesStrategy on processResources (the explicit
  resources.srcDir re-adds the default root, which now doubles the bundled scripts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mApqAbr8vkLC7gUDjavu6
2026-07-27 15:40:50 +00:00
Vitor PamplonaandClaude Opus 5 8bb074c6ae fix(buzz): refresh the roster after a role change, and clear unread on open
**The roster went stale.** Promoting somebody changed nothing on screen until the
next cold start. Buzz signs its 39000-39003 with `d`/`p` tags and **no `h`**, yet
stores and fans them out channel-scoped — so a filter carrying `#h` does not match
their tags, and one without `#h` is a global subscription, which by design receives
no channel-scoped event. Neither shape can be live; measured it directly, a role
change delivers only the kind-40099 that narrates it.

So use that as the cue: the members screen re-reads the group's state whenever a
new system message lands in it, keyed on the message id so it fires once per
change rather than polling. Account.refreshRelayGroupState does the fetch.

**Unread badges never cleared.** loadAndMarkAsRead lived inside NormalChatNote —
the `else` of the render switch — so a row drawn by any specialised path (Buzz
system lines and activity rows, diffs, forum votes, NIP-28 admin lines, zaps)
never advanced the room's last-read marker. On a Buzz relay that is most rows:
joins, adds and role changes are all system messages, so a channel whose newest
events were those kept its badge no matter how often it was opened. Hoisted the
call to cover every row type; NormalChatNote's now-dead routeForLastRead
parameter is gone.

Verified on emulator-5554 against nosfabrica.communities.buzz.xyz: promoting a
member now shows the `admin` badge without restarting the app, and opening
`general` cleared its badge while the channels left untouched kept theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:38:50 -04:00
Claude 052da60f9c feat(quartz): index missing SearchableEvent kinds and tag-borne text
Massive review of every Event subclass in Quartz that carries
human-authored plaintext at rest but was not participating in the NIP-50
full-text index (SearchableEvent).

Add SearchableEvent to 15 kinds whose content is plaintext searchable
text a user would look for:
- 9737 Bolt12 zap intent (comment) — mirrors 9734/9736
- 5302 / 5303 NIP-90 content / people search request (search query)
- 31871 / 31872 attestation / attestation request — sibling family was
  already searchable
- 1315 roadstr road-event report (free-text comment)
- 45001 / 45003 Buzz forum post / comment (body)
- 48106 Buzz huddle guidelines
- 30176 / 30175 / 30177 / 10100 Buzz team / persona / managed-agent /
  agent-profile (name, description, system prompt)
- 30620 Buzz workflow definition (name + YAML)
- 3302 Concord chat edit (replacement message text) — mirrors kind-9 chat

Widen indexableContent() on 8 kinds that already implemented
SearchableEvent but dropped natural-language text carried in tags:
- 30020 auction — category hashtags were written by build() but never
  indexed
- 12473 Birdex — species names
- 30382 contact card — public topics
- 9002 NIP-29 group-metadata edit — hashtags
- 30054 Podcasting-2.0 episode — topics
- 38192 PS1 save — region name
- 1111 comment / 1311 live-activity chat — hashtags

Encrypted-at-rest (NIP-04, giftwraps, MLS/marmot, NWC, cashu), purely
structural (relay/follow lists, reactions, deletions, moderation/presence
signaling), and ephemeral events were reviewed and deliberately left out.
The NIP-31 alt tag was also left out: it is frequently kind-level client
boilerplate and would dilute relevance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHSTACrFHaRX1o6C5cEk6N
2026-07-27 15:33:11 +00:00
Vitor PamplonaandClaude Opus 5 ceb9f9c3db feat(buzz): dock the member search at the bottom, and fix promotions on Buzz
**Adding people.** The Members screen hid its search behind a FAB, so adding a
handful of people was "open dialog, search, pick, dialog closes, reopen" per
person. The field now lives at the bottom of the screen with its results rising
above it, like a chat composer: each pick lands in the roster above and clears
the query while the keyboard stays up. It clears the gesture bar and rides above
the IME, so the field you type into is not the part that gets covered.

**Promotions did nothing.** "Make moderator" published a kind-9000 and changed
nothing, on either client. Two reasons:

- NIP-29 carries roles inside the `p` tag; Buzz reads a top-level `role` tag
  (`extract_tag_value(event, "role")`) and defaults to `member` without it. So
  every promotion re-added the target as a plain member. PutUserEvent can now
  carry that tag and Account maps our role onto Buzz's vocabulary before sending.
- That vocabulary is `owner`/`admin`/`member`/`guest`/`bot` — there is **no
  moderator**, and a role the relay cannot parse fails the whole put-user. So the
  action is hidden on Buzz rather than offered and silently dropped.

**The owner could not promote anyone.** membershipOf only mapped the literal
`admin` to ADMIN, but a Buzz channel's creator carries `owner` — leaving the one
person with full authority ranked below it, so "Make admin" never appeared. Both
role strings now mean ADMIN.

**The 3-dot button moved when tapped.** An expanded DropdownMenu still emits a
node into its parent, and it sat as a direct child of a `spacedBy(12.dp)` Row —
so opening the menu added a second gap and shoved the button sideways. Button and
menu now share a Box. ConcordMembersScreen had the identical bug and is fixed
too; GitBrowseUi looks like a third instance and is left alone as unrelated
territory.

Verified on emulator-5554 against nosfabrica.communities.buzz.xyz: promoting the
added member published the 9000, the relay narrated it, and after the roster
refreshed the member carries an `admin` badge. The 3-dot sits at the same pixel
column whether the menu is open or closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:14:29 -04:00