Commit Graph
17808 Commits
Author SHA1 Message Date
David KasparandGitHub 9153ca4e92 Merge pull request #3740 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 16:21:55 +02:00
davotoulaandgithub-actions[bot] 78ad9aaa77 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-27 14:20:46 +00:00
David KasparandGitHub 6d8b2d96e2 Merge pull request #3739 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 16:20:09 +02:00
davotoula e09cb521d9 update cs,sv,de,pt 2026-07-27 16:10:48 +02:00
vitorpamplonaandgithub-actions[bot] 9855abda64 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-27 12:59:14 +00:00
Vitor PamplonaandGitHub 1705963939 Merge pull request #3733 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 08:56:20 -04:00
vitorpamplonaandgithub-actions[bot] 402122727e chore: sync Crowdin translations and seed translator npub placeholders 2026-07-27 04:43:01 +00:00
Vitor PamplonaandGitHub 5d7b629104 Merge pull request #3738 from vitorpamplona/claude/quartz-test-warnings-6wptk1
test(quartz): clean up compiler warnings across the test suites
2026-07-27 00:40:13 -04:00
Claude 03eeb02378 test(quartz): clean up compiler warnings across the test suites
Resolve all `:quartz:compileTestKotlin*` warnings without changing any
test's behavior:

- Drop redundant bare `Secp256k1Instance` "force crypto lib load"
  statements (and their now-unused imports). JVM object initialization is
  thread-safe and lazy, and every one of these tests exercises crypto, so
  the lib loads on first use regardless — the eager reference was a no-op
  the K2 compiler flags as an unused expression.
- PairingEventTest: use `assertIs` instead of `assertTrue(x is T)` + cast.
- MergeQueryCorrectnessTest: drop `!!` that smart-cast already made moot.
- PodcastCommentScopeTest / MintExceptionTest: widen the declared type so
  the `is` checks are genuine runtime checks rather than always-true.
- FetchAllIdleTimeoutTest / NostrConnectSignerServiceTest: opt in to
  `ExperimentalCoroutinesApi` at the class level.
- GiantReqStreamTest: name the `WebSocketListener` overrides' parameters
  to match the supertype.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GyXakbYay4zNJ4cwoF4j7N
2026-07-27 04:10:53 +00:00
Vitor PamplonaandGitHub 7956648f92 Merge pull request #3735 from vitorpamplona/fix/community-tab-first-frame
fix(chats): render the community tab correct on its first frame
2026-07-26 22:48:56 -04:00
Vitor PamplonaandGitHub 9e0260df2b Merge pull request #3736 from vitorpamplona/claude/buzz-community-redesign-vrovav
Redesign Buzz channel rows to match Concord style with activity previews
2026-07-26 22:48:40 -04:00
Vitor PamplonaandGitHub 20f00228d7 Merge pull request #3737 from vitorpamplona/claude/concord-channels-banner-to0nmb
Remove community banner from Concord home screen
2026-07-26 22:48:03 -04:00
Vitor PamplonaandClaude Opus 5 654c33fee8 fix(chats): render the community tab correct on its first frame
Leaving the Buzz community tab and coming back rebuilt the screen: Direct
Messages sat empty for about a second, and the channel list settled into a
different order than it had a moment earlier — every visit, and a different
order each time.

Three causes, all fixed here.

**The tab was destroyed, not left.** navBottomBar popped siblings without
`saveState`, so the entry — and its ViewModelStore — was thrown away on every
tab switch. Returning built new BuzzRelayImportViewModel / BuzzDmListViewModel
instances, whose bind() self-guard could not help because the guard lives on an
object that no longer existed. Adding saveState/restoreState keeps each tab's
state; other tabs get their scroll position back as a side effect.

**The DM inbox waited on the network to show what it already had.** bind()
called refresh() → discoverMemberChannels(), an 8s-timeout relay round-trip,
before anything could render — even though rebuildRows() reads nothing but
LocalCache and an in-memory map, and the always-on BuzzDmDiscovery has already
recorded those channel ids process-wide in BuzzDmChannels. Seed memberChannels
from that registry and project the rows before any network work; refresh still
runs behind it and corrects anything stale.

**The channel order was arrival order.** buzzGroupIds is "membership ids as the
ViewModel emitted them, then directory ids", and the only sort was
`sortedByDescending { it.id in starred }` — a stable sort over a boolean, which
preserves whatever landed first. Order by (starred, name) so the first frame is
the final order; a channel whose 39000 hasn't arrived sorts by id until its name
lands.

Verified on emulator-5554, capturing ~0.45s after the tab switch: channels in
alphabetical order and both DMs present, with no reshuffle in later frames.
Previously the same capture showed an empty Direct Messages section and a list
that reordered within the second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:45:16 -04:00
Claude fc758e889b fix: remove community banner from Concord channels list
Drop the CORD-02 §6 banner hero that showed when a community was expanded
to OPEN in the Concord home screen, so opening a community and viewing its
channels no longer displays the banner. Removes the now-unused CommunityBanner
composable and its ContentScale/height imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6rz1csKDADKc5nr7R4a1b
2026-07-27 02:42:32 +00:00
Vitor PamplonaandGitHub bce35074fc Merge pull request #3734 from vitorpamplona/claude/cashu-token-claim-error-6s6dym
Support P2PK witness signing for pasted Cashu tokens
2026-07-26 22:28:23 -04:00
Vitor PamplonaandGitHub da41ba0b79 Merge pull request #3732 from vitorpamplona/fix/buzz-forum-post-only-in-forum-channels
fix(buzz): only offer "new thread" where a forum post belongs
2026-07-26 22:09:19 -04:00
Claude 3c440a6d27 fix(cashu): case-insensitive P2PK lock match + all-or-nothing redeem
Two bugs found auditing the P2PK redeem path:

- Hex case: a lock's `data` pubkey is sender-formatted and NUT-11 doesn't
  mandate a case, but our key index is keyed by lowercase x-only (Hex.encode
  is lowercase). An uppercase/mixed-case lock we actually hold the key for was
  falsely rejected as unredeemable. Normalize the lock to lowercase before the
  lookup, and compare identity-key locks case-insensitively.
- Multi-mint partial redeem: callers redeem one mint-group at a time, each
  swapping + publishing. An unsignable P2PK lock in a later group threw only
  after earlier groups were already spent + published, leaving a half-redeemed
  state the user was told had failed. Add `firstUnsignableP2pkLock` /
  `requireP2pkRedeemable` and pre-flight every group before redeeming any,
  mirroring the existing unknown-mint pre-check (wallet ViewModel + amy CLI).

Also document that P2PK.signWitness's `["P2PK"` prefix guard is load-bearing
for safety (prevents cross-protocol signature reuse when signing with the
identity key), not just for parsing. Adds tests for case-insensitive matching
and the pre-flight helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKeRaX749TYnJ7oR8UpqA4
2026-07-27 01:26:36 +00:00
Claude ee5efba88d perf(cashu): avoid needless wallet-key decrypt and secret parse on redeem
Follow-up to the P2PK redeem support:

- Gate the redeem signing-key gathering behind an actual P2PK lock. The
  wallet P2PK key is decrypted from kind:17375 via the signer — a network
  round-trip on a NIP-46 bunker (and a possible approval prompt). The common
  case (a plain, unlocked token) needs none of it, so only fetch keys when
  `anyP2pkLocked()` is true. Applies to both the wallet ViewModel and the amy
  CLI token-redeem path.
- Fast-reject in `P2PK.parseSecret`: NUT-10 well-known secrets are JSON
  arrays, so bail before the throwing JSON parse when the string isn't one.
  Redeem parses every proof's secret once, so this drops a thrown+caught
  exception per plain proof (also benefits the nutzap redeem path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKeRaX749TYnJ7oR8UpqA4
2026-07-27 01:21:29 +00:00
Vitor PamplonaandClaude Opus 5 3acec7ee87 fix(buzz): don't offer a canvas on a DM unless one already exists
The Canvas icon showed on every Buzz channel including DMs, and its edit FAB
offered to create one there. Buzz itself never does: its canvas entry needs
`hasCanvas || canEditNarrative`, and `canEditNarrative` is
`canManageChannel && selfMember !== null && channelType !== "dm"` — so a DM can
only ever *display* a canvas that already exists, never write one
(ChannelManagementSheet.tsx). We were advertising "start a shared document" in a
two-person conversation, and a canvas created there would have been ours alone
to see.

Mirror both halves: a DM gets the icon only when a canvas is already in cache,
and the canvas screen drops its edit FAB for DMs so what remains is read-only.
Non-DM Buzz channels are unchanged — icon always, editing always.

The has-a-canvas check reads the same BuzzWorkspaceStates registry the canvas
screen renders from and recomposes on `canvasUpdates`, so the icon appears when
the document lands rather than on the next visit.

Verified on emulator-5554: the Buzz DM with Vitor (no canvas) shows only the
overflow, where it used to show Canvas + overflow; `general` on the same relay
still shows Canvas + overflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 21:20:05 -04:00
Vitor PamplonaandClaude Opus 5 7d4e4f1cc9 fix(buzz): only offer "new thread" where a forum post belongs
The Threads compose FAB keyed on relay dialect, not channel type: on any Buzz
relay it published a kind-45001 forum post into whatever channel you were in.
Buzz only ever creates those in a `t=forum` channel — its client mounts the
forum view for `channelType === "forum"` alone — so a 45001 in a `t=stream`
chat channel is a post that no Buzz user can see. The relay accepts it
(nothing there gates 45001 by channel type), which is exactly why the client
has to.

Gate the FAB on the channel's declared type when the host speaks Buzz, and
leave every other relay alone: there the same button writes a NIP-7D kind-11,
which is valid in any group. A Buzz channel whose kind-39000 hasn't landed yet
reads as null and fails closed — a FAB appearing a moment later beats
publishing into the wrong channel.

Reading is untouched. An existing thread still renders wherever it came from;
this only removes the offer to create one where it would be invisible.

The empty state said "No threads yet. Start one with the + button." while the
FAB was hidden, which was already wrong for non-members and is now wrong for
chat channels too. Added a read-only variant.

The gate is a named function rather than an inline condition so the allow-path
has a test: no relay reachable in a manual pass exposes a `t=forum` channel, and
a gate that only ever denies is indistinguishable from deleting the feature.
Denials are covered on-device — a Buzz stream channel loses the FAB, a
NIP-29 group (basspistol.org) keeps it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:48:41 -04:00
Claude 63ff055b53 feat(cashu): claim P2PK-locked tokens and clear errors when we can't
Pasting a P2PK-locked cashu token (NUT-11) into the wallet sent the proofs
to /v1/swap with no witness, so the mint rejected them with an opaque
`witness is missing for p2pk signature` 400. Only the NIP-61 nutzap path
signed witnesses; the generic redeem path had no P2PK support at all.

- quartz: add `signP2pkWitnesses` (pure, resolver-driven) + the
  `P2PKUnredeemableException` it throws when a locked proof's key is
  unknown, and a `CashuMintOperations.redeemToken` that signs then swaps.
- commons: `CashuWalletOps.redeemToken` now takes the wallet P2PK key and
  (local-signer-only) identity key, indexes them by x-only pubkey, and
  routes through the P2PK-aware path. Add `describeRedeemError`, which tells
  a user whose token is locked to their own identity key (e.g. Bey Wallet's
  P2PK send) — but who is on a bunker/external signer that can't sign a raw
  witness — to import their nsec elsewhere to claim it.
- amethyst: `CashuWalletState.redeemSigningKeys()` surfaces both keys
  (identity key only for a local NostrSignerInternal); the wallet ViewModel
  wires them in and reports via `describeRedeemError`.
- cli: `amy cashu receive token` passes the same keys and reports a distinct
  `p2pk_locked` error code.

Adds P2PKRedeemTest covering pass-through, x-only + compressed locks,
verifiable witnesses, and the unredeemable case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKeRaX749TYnJ7oR8UpqA4
2026-07-26 23:39:49 +00:00
Vitor PamplonaandGitHub ccc6b804ae Merge pull request #3731 from vitorpamplona/docs/buzz-readme-kind-conflicts
docs(quartz): correct the Buzz kind-conflict table and document how to read Buzz's source
2026-07-26 19:25:20 -04:00
Vitor PamplonaandClaude Opus 5 6a5d55aa9b docs(quartz): correct the Buzz kind-conflict table and document how to read Buzz's source
Two rows of "Kind conflicts — implemented but NOT registered in EventFactory" were
stale: 20001 and 39005 are both dispatched now, disambiguated by tag shape inside
the kind's block (`g` for BitChat presence, `h` for a Buzz thread summary). Split
the table into the disambiguated pair and the three where the incumbent really does
keep the slot, and record why the 39005 signal is safe — the NIP-29 relay-generated
39xxx family is `d`-addressed and never emits `h` — plus the fact that nothing
throws when it is wrong, so the failure is silent.

Also document reading Buzz's Rust without a checkout, which currently costs everyone
the same detour: KDoc across this package cites crate-relative paths
(`buzz-relay/src/handlers/...`) while the repo puts everything under `crates/`, and
`raw.githubusercontent.com` 404s on those paths with plain curl usually sandboxed —
`gh api ... contents/... | base64 -d` is the way in. Notes where the answers live
(handlers for what the relay emits, buzz-db/channel.rs for channel_type and the two
visibility values, desktop/src/features for what Buzz's own client renders — which
decides whether an event we publish is visible to anyone), and that the relay's tests
are the best spec: `channel_scoped_content_kinds_require_h_tags` is what establishes
that canvas and the forum kinds are per-channel, not per-workspace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:18:53 -04:00
Vitor PamplonaandGitHub 552479a637 Merge pull request #3728 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-26 19:14:42 -04:00
Vitor PamplonaandGitHub 81f0c0dd1b Merge pull request #3729 from vitorpamplona/refactor/relay-group-topbar-overflow
refactor(chats): demote Threads and Share to the channel overflow menu
2026-07-26 19:14:35 -04:00
Vitor PamplonaandClaude Opus 5 59646917e9 refactor(chats): demote Threads and Share to the channel overflow menu
The relay-group top bar carried four affordances — Threads, Canvas, Share and
the overflow — which crowded the title to the point that a channel called
personalized-knowledge-graphs rendered as "personalized-..." with its relay
truncated too.

Threads was the worst offender because it advertises a feature that is usually
not there. On a Buzz `t=stream` channel the threads list is forum posts (kind
45001), and Buzz only ever creates those in `t=forum` channels — which the
relay's channel list already surfaces in their own Forums section, opening
straight into this same view. So on every chat channel the button led to "No
threads yet", and its + would have published a 45001 into a stream channel,
where Buzz's own client never renders it (it mounts ForumChannelContent only
for channelType === "forum"). It stays in the menu rather than being gated off,
because on vanilla NIP-29 relays the same screen is the legitimate NIP-7D
kind-11 thread view.

Canvas keeps its icon: it is this channel's shared document — content — while
Threads and Share are navigation you reach for occasionally.

Also fixes the overflow only existing in the member branch: a pending join or a
gated group you don't belong to replaced the whole menu with the Join button, so
Members was unreachable while browsing. The menu is now always present, with the
membership actions (Members/Edit/Invite/Leave) gated as before and Threads/Share
always available — you can want to hand out a group you are still only browsing.

Canvas is per-channel, not per-workspace: the relay requires an `h` channel tag
on kind-40100 (its own channel_scoped_content_kinds_require_h_tags invariant),
so correct the empty state from "shared in this workspace" to "in this channel",
say so in the KDoc, and name the channel under the title the way Threads does.

Verified on emulator-5554: Buzz channel shows Canvas + overflow (Threads, Share,
Members, Leave) and the full title now fits; a non-Buzz group (basspistol.org)
shows only the overflow with the same four items; the canvas screen names its
channel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:59:04 -04:00
Claude eff0a30a39 perf: skip chat warmup + activity preview for Buzz forum rows
Forum channels store their posts as threads (a separate store), not in the
chat notes the activity preview reads — so a forum row was opening a kind-9
chat warmup subscription that returns nothing and could never render a
last-message preview, facepile, or unread badge.

Add a `showActivityPreview` flag (default true) to BuzzImportRow that gates
the warmup, the notes-flow collection, and the preview/facepile/unread reads.
The forum section passes false, so a forum row skips the useless subscription
and shows a member-count summary instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHnm6G9YytnLfs1ycYfK89
2026-07-26 22:57:49 +00:00
Claude 471a085b14 Merge remote-tracking branch 'origin/main' into claude/buzz-community-redesign-vrovav
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt
2026-07-26 22:50:56 +00:00
vitorpamplonaandgithub-actions[bot] 8a284278fa chore: sync Crowdin translations and seed translator npub placeholders 2026-07-26 22:29:10 +00:00
Vitor PamplonaandGitHub bc1cce6065 Merge pull request #3727 from vitorpamplona/feat/buzz-system-message-sentences
feat(buzz): say who joined and what changed in the kind-40099 system lines
2026-07-26 18:26:03 -04:00
Vitor PamplonaandClaude Opus 5 8df98c34df feat(buzz): say who joined and what changed in the kind-40099 system lines
The in-chat markers read "member joined" / "visibility changed" — the relay's
raw payload type with the underscores swapped for spaces. Everything that makes
the line useful was parsed and then dropped: which member, who added them, and
what the setting changed TO.

Render the full sentence from the signed payload instead, with the pubkeys
resolved to the names the viewer knows them by and the avatar of whoever the
line is about:

  member joined      -> "Shawn was added by straycat"  (actor != target)
                     -> "Vitor joined"                 (self-join, actor == target)
  member removed     -> "Bob was removed by Alice"
  visibility changed -> "straycat made this channel open — anyone can find and join it"
                     -> "... made this channel private — invite only"
  ttl changed        -> "Alice set messages to disappear after 7 days" / turned off
  topic/purpose      -> the new value, or "cleared the topic" when blank
  channel created/deleted/archived/restored, message deleted (+ public reason),
  dm created         -> named after their actor

The event is signed by the RELAY keypair, so the people come from the payload's
actor/target, never from note.author. Membership lines are about the member, so
that is whose avatar shows and whose profile the pill opens; everything else is
about the actor.

Also covers the neighbouring timeline narration, which had the same problem:
huddle joins/leaves now name the participant from the `p` tag instead of
"someone joined the huddle", job lines name the signer, and forum votes name
the voter. All of these move from hardcoded English into string resources.

Quartz's SystemMessagePayload was missing target_event_id, action_id,
reason_code, public_reason and participants — every field of the moderation
tombstone and the DM-created payload. The KDoc now carries the complete
vocabulary read off the relay's emit_system_message callers, and the type
strings are constants so the UI cannot typo a branch into dead code.

An unknown type still renders as "alice: some_new_type" rather than vanishing,
so a relay that grows new vocabulary stays legible.

Verified on emulator-5554 against nosfabrica.communities.buzz.xyz: the four
"was added by" lines, "straycat created this channel", "Matthias Debernardini
joined" and "straycat made this channel open" all render with the right subject
avatar, in the chat and in the Messages-list preview.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:19:01 -04:00
Vitor PamplonaandGitHub 2a25dfee01 Merge pull request #3724 from dergigi/fix/decode-numeric-html-entities
fix: decode numeric HTML entities in link preview meta tags
2026-07-26 18:11:33 -04:00
Vitor PamplonaandGitHub a0cf0ec332 Merge pull request #3726 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-26 18:04:20 -04:00
vitorpamplonaandgithub-actions[bot] f17205b015 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-26 21:57:06 +00:00
Vitor PamplonaandGitHub 14381984a7 Merge pull request #3725 from vitorpamplona/fix/event-factory-39005-collision
fix(quartz): route kind-39005 by tag shape so Buzz thread summaries stop parsing as NIP-29 pin lists
2026-07-26 17:54:25 -04:00
Gigi 4cb0fb64af fix: decode numeric HTML entities in meta tag content
Link previews left &#34; / &#x22; literal because replaceCharRefs only
whitelisted named entities. Parse terminated numeric refs as code points.

Fixes #3723
2026-07-26 23:52:33 +02:00
Vitor PamplonaandClaude Opus 5 75a5e91681 fix(quartz): route kind-39005 by tag shape so Buzz thread summaries stop parsing as NIP-29 pin lists
kind:39005 is claimed by two unrelated relay-signed addressable events, and
EventFactory mapped it unconditionally to NIP-29's GroupPinnedEvent, leaving
Buzz's ThreadSummaryEvent unreachable:

  GroupPinnedEvent    d = group id, e = pinned message ids, no h, empty content
  ThreadSummaryEvent  d = thread root id, e = that root, h = channel, JSON content

Nothing throws on the mismatch, so a summary would have parsed as a pin list and
pinnedEventIds() would have reported the thread root as a pinned message.

Discriminate inside the kind's block on the `h` tag, following the kind-20001
BitChat/Buzz presence precedent already in this file. `h` is a safe signal in
both directions: the whole NIP-29 relay-generated 39xxx family (metadata,
admins, members, participants, supported-roles, pinned) is addressed by `d`
alone and never emits `h`, and neither builder does either, so both classes
round-trip through the factory on outbound signing as well.

Buzz publishes summaries live to channel subscribers (not only over its HTTP
bridge), so this is what makes it safe for a channel-scoped filter to ask for
39005 at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:52:12 -04:00
Vitor PamplonaandGitHub 38b085812d Merge pull request #3721 from vitorpamplona/fix/tor-guard-sample-self-heal
fix(tor): self-heal a guard sample that is rotten at runtime, not just on disk
2026-07-26 17:20:43 -04:00
Vitor PamplonaandGitHub 871d56e1a4 Merge pull request #3717 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-26 17:20:15 -04:00
Vitor PamplonaandGitHub 221cb84408 Merge pull request #3720 from vitorpamplona/claude/bolt12-zaps-nip-naming-qzw8zi
Rename NIP-XX to NIP-B1 for BOLT12 Zaps specification
2026-07-26 17:20:04 -04:00
vitorpamplonaandgithub-actions[bot] 6bf3c11173 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-26 21:18:45 +00:00
Vitor PamplonaandClaude Opus 5 e934d41bb2 fix(tor): self-heal a guard sample that is rotten at runtime, not just on disk
Tor wedged across app restarts with ~87% of relay connections failing, and
neither existing recovery fired. Captured from the device:

  guards.json  default: 60 guards, 59 disabled, 1 unlisted -> 1 usable
  Arti log     AllGuardsDown { n_accepted: 0, n_rejected: 60 }  x21,490
  sockets      36,007 failures vs 5,276 successful opens

Everything above Tor degraded with it. Concord was the visible casualty: its
control-plane sync drained exactly 12 wraps on every launch and never folded, so
the Messages tab showed zero Concord rows for two restarts running.

Two recoveries exist and this state slipped between both:

- `ArtiGuardState.hasNoUsableGuards` required `usable == 0`. Its own KDoc claimed
  "a single usable guard is enough to recover" — the device disproved it. One
  survivor that is merely *unreachable* is useless for recovery but sufficient to
  veto it, so the wipe never ran.
- `TorManager`'s watchdog only arms while status sits at Connecting. Tor had
  bootstrapped: the SOCKS proxy was bound, `hasEverBootstrapped` was true, ~13% of
  connections still worked, so status reached Active and the watchdog never fired.
  It is built for "Tor never came up"; this is "Tor came up and its guards rotted".

Nothing observed the steady-state failure rate, so all three gates evaluated the
same way on every launch and `guards.json` carried the wedge forward forever.

1. Proportional disk rule. A sample of at least MIN_SAMPLE_TO_JUDGE_RATIO whose
   usable guards fall under 1/USABLE_RATIO_DIVISOR of the total is wedged. Below
   that size only the strict `usable == 0` rule applies, so a young sample Arti is
   still filling is never wiped out from under a legitimate first bootstrap.

2. Runtime detector. `TorService` counts Arti's own AllGuardsDown log lines
   (GUARDS_DOWN_THRESHOLD within GUARDS_DOWN_WINDOW_MS) and exposes
   `TorBackend.guardsDownSignal`; `TorManager` routes it through the same
   rate-limited self-heal to `resetWithCleanState()`. This is the general fix: it
   believes Arti when it says every guard was rejected, so it fires even while
   `guards.json` still looks healthy and status is Active — precisely the blind
   spot between the two older heuristics. The count lives in the log callback
   because that is the only place Arti surfaces it; the reset stays in TorManager,
   which owns the cadence.

Verified on the wedged device. Next launch, unprompted:

  W TorService: No usable Arti guards left on disk — wiping state to rebuild the guard sample

  guard sample   60 total / 1 usable   ->  20 total / 20 usable / 0 disabled
  AllGuardsDown  21,569                ->  0
  sockets        36,007 fail / 5,276   ->  444 fail / 232 open
  Concord wraps  12, 12, 12 (pinned)   ->  12 -> 37 -> 258
  Concord rows   0                     ->  4 and climbing

Tests cover the 59-of-60 field case, a small-sample false-positive guard, and a
healthy-majority sample. The pre-existing 22-guard "poisoned but not wedged"
fixture still passes, so the ratio does not regress the earlier variant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:18:26 -04:00
Vitor PamplonaandGitHub dfbe2b9e61 Merge pull request #3718 from vitorpamplona/fix/buzz-minichat-kind9-replies
fix(buzz): thread kind-9 replies into the minichat instead of the channel
2026-07-26 17:16:04 -04:00
Vitor PamplonaandGitHub eedc8c7a08 Merge pull request #3719 from davotoula/fix/audio-player-overflows-note-layout
Stop the inline audio player painting over the note
2026-07-26 16:43:33 -04:00
Claude 67223d612f refactor: rename BOLT12 zaps placeholder NIP to the assigned NIP-B1
The BOLT12 zaps feature was built against a placeholder NIP identifier
("nipXX" / "NIP-XX", and "NIP-2421" in one plan). The number NIP-B1 has
now been assigned, so update the naming across every module:

- rename the Quartz package `nipXXBolt12Zaps` -> `nipB1Bolt12Zaps`
  (commonMain + commonTest) and every import referencing it.
- KDocs/comments: `NIP-XX` -> `NIP-B1` in quartz, commons, amethyst, cli.
- wire binding prefix: `nostr:nipXX:` -> `nostr:nipB1:`
  (Bolt12ZapValidator.NIP_URI_PREFIX, NIP-47 pay `payer_note`, tests).
- KindNames: Bolt12 Zap / Bolt12 Offers NIP number "XX" -> "B1".
- plan doc references `NIP-2421` -> `NIP-B1`.

Leaves the unrelated `nipXXPodcasting20` package and the audio-rooms
draft (also placeholder "NIP-XX") untouched. quartz main + test compile
and spotless is clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012P3krSe92wicpswBr2CP9s
2026-07-26 20:32:16 +00:00
Vitor PamplonaandClaude Opus 5 07e3f21509 fix(chats): light the Messages badge for every row type, notify Buzz thread replies
Two halves of the same gap: a row could show its blue dot while nothing above it
agreed.

1. Bottom-bar envelope counted only DMs
------------------------------------------------------------------------------
`messagesHasNewItems` mapped each Messages row through `unreadPrivateChatRoute`,
which opens with `if (newestMessage !is ChatroomKeyable) return null`. Only
NIP-17/NIP-04 DM events implement that interface, so eight of the nine row types
were silently skipped — public chats, ephemeral rooms, geohash cells, Marmot
groups, NIP-29/Buzz channels, Concord channels, and both collapsed "grouped"
rows. Each of those rows already computed its own dot from its own last-read
route; the badge just never asked.

`rowHasUnreadFlow` now answers, per row, the same question the row composable
answers for itself, keyed off the note's gatherer (a Buzz channel and a Concord
channel can both carry a kind-9, so event kind alone can't tell them apart). It
returns a Flow rather than a (route, createdAt) pair because the two collapsed
rows fan in over every child channel — approximating those by their newest child
would miss an older channel that is still unread.

2. Buzz thread replies notified nothing
------------------------------------------------------------------------------
Buzz's clients thread with `["e", <id>, "", "reply"]` and only ever `p`-tag
@mentions, so a reply to my message names me nowhere. `isNotifiablePublicChatRep
ly` — the rule that lets a reply notify without a `p` tag — bails unless the
event is a ChannelMessageEvent (kind 42), so a kind-9 thread reply qualified
under nothing. Combined with thread replies now being kept out of the channel
timeline and its unread dot, a reply to my message in a Buzz channel had become
invisible on every surface.

`isBuzzThreadReplyToMyEvent` mirrors the fix already used for Buzz reactions
(`isReactionToMyEvent`): when a chat event carries no `p` tag, resolve the author
of its `root`/`reply` marked `e` targets instead of trusting a tag. Deliberately
only the MARKED targets — a bare `e` is WhiteNoise/Marmot's in-chat reply, not a
thread. It is OR'd into the same three gates the reaction case uses, so a reply
from a channel member I don't follow still notifies.

Also admits kind-40002 into NOTIFICATION_KINDS: nothing writes it any more, but
legacy Buzz thread replies exist and were being dropped at the kind gate before
any relevance check ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:22:22 -04:00
davotoulaandClaude Opus 5 3813e248c2 i18n: translate Buzz, Blossom import and BOLT12 strings into cs, de, pt-BR, sv
Fills the on-disk translation gap for the recently merged Buzz workspaces,
Blossom file import and BOLT12 offers features, plus the channel-invite and
payment-notification strings.

Inserted per-locale (128 cs, 124 de-rDE, 126 sv-rSE, 128 pt-rBR) driven off
each file's own diff rather than a shared union block, since Crowdin strips
source-identical keys asymmetrically across locales.

All 7 new <plurals> are covered in every locale; Czech gets the full CLDR
one/few/many/other set. Source-identical entries (Canvas, Workspace, OK,
LIVE, Media, Social, Reposts, and the bare %1$d+ count formats) are
deliberately left out — Crowdin strips those on export and Android falls
back to values/strings.xml at runtime.

Verified: no duplicate keys, well-formed XML, and format-placeholder parity
with the English source across all four files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T45dBdJ8GRBy8zw9yDQRPW
2026-07-26 22:21:24 +02:00
Vitor PamplonaandClaude Opus 5 4f6e16a74e fix(nip29): line up preview, timeline and unread on one predicate
A minichat thread reply was showing as a group's "last message" on Messages, and
lighting its unread dot, even though the channel timeline correctly hides it.
Three surfaces disagreed about what counts as a message:

  channel timeline  ChannelFeedFilter   !isMinichatReply(..) && isAcceptable
  Messages preview  newestChatNote      isGroupChatContent()  && isAcceptable
  unread dot        hasChatNewerThan    isGroupChatContent()  && isAcceptable

So a reply that the channel deliberately routes to its thread still became the
row summary, and still lit the dot — you would open the group, see nothing new,
and watch the dot clear. The Concord side already solved exactly this by sharing
one predicate (`isConcordTimelineMessage`) across feed, preview and badge; this
gives NIP-29 the same treatment:

    isRelayGroupTimelineMessage = isGroupChatContent && !isMinichatReply && isAcceptable

Now used by:
- `newestTimelineNote` (replaces the local `newestChatNote`) for the row preview,
  in both INLINE and GROUPED view modes
- both additive paths in ChatroomListKnownFeedFilter, so an arriving reply cannot
  bump a row either
- `hasChatNewerThan`, which backs the per-channel dot (`relayGroupChannelHasUnread
  Flow`, also used by the workspace channel list), the collapsed per-relay dot
  (`relayGroupServerHasUnreadFlow` composes it), and transitively the Messages row
  dot, which reads the createdAt of the note the preview picked

The bottom-bar Messages badge follows automatically: it derives from the feed rows
themselves. (It only counts private DMs today — `unreadPrivateChatRoute` returns
null for anything that isn't ChatroomKeyable — which is a separate, pre-existing
scope decision, untouched here.)

Verified on device by creating the case rather than waiting for it: posted a reply
into a thread so it became the newest event in the channel. The thread shows it
(chip 4 -> 5 replies), the channel timeline does not, and the Messages row still
previews the newest timeline event. Before this change that reply would have been
the row summary. The earlier screenshot only looked right because a system message
happened to be newer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:00:49 -04:00
Vitor PamplonaandClaude Opus 5 ec0c4a6c2a fix(buzz): thread kind-9 replies into the minichat instead of the channel
A reply written by any current Buzz client is a kind-9 carrying a NIP-10
`reply`-marked `e` tag. Amethyst rendered it as a flat row in the main channel
with the parent quoted above it, and it never appeared in the thread on its
parent — the exact inverse of where Buzz puts it. Observed on the wire:

  parent  kind 9  tags: [h, <channel>]
  reply   kind 9  tags: [h, <channel>], [e, <parent>, "", "reply"]

`isMinichatReply` is the single definition three consumers share — the channel
timeline filter drops these, the reply-count chip counts them, and the minichat
feed shows them — but it was type-gated to CommentEvent (1111) and
StreamMessageV2Event (40002), so a kind-9 ChatEvent fell through to `false`.
Every downstream behaviour followed from that one gap: the reply stayed in the
timeline, the parent showed no "N replies" chip, and the thread was empty.

Accepting a marked `e` on kind 9 is NIP-C7 compliant. C7 defines exactly one
reply mechanism for kind 9 — `["q", <id>, <relay>, <pubkey>]` — and never
mentions `e` at all, so a marked `e` carries no C7 meaning and is free to denote
a thread reply. Matching on the MARKER (never the bare tag) is what keeps
WhiteNoise/Marmot working: they thread kind-9 chat with a plain, unmarked `e`,
which is an in-chat reply and must keep rendering as a quote bubble. Tests pin
all four cases: marked direct, marked nested (root+reply), unmarked, and `q`.

Also stop writing kind-40002 for Buzz minichat replies. Nothing in Buzz writes
40002 any more: every send path in their mobile, desktop and CLI clients emits
kind 9, the ~50 remaining references are all reads (filter kind lists, feed
query sets, archive constants), and their NOSTR.md grades kind:9 as supported
against 40002's "Buzz-only — no standard NIP-29 client renders these". It is a
read-compat tail from the 10002 -> 40001 -> 40002 migration, and Amethyst was
the last active writer — so our replies threaded nowhere but our own client.
We now emit kind 9 with tags byte-identical to Buzz's `_buildReplyTags`
(direct -> one `reply` marker; nested -> `root` + `reply`), which is what
`buzzThread` already produced. Reading 40002 stays supported for events already
in the wild, including the ones we wrote.

Deliberately NOT changed: `computeReplyTo`'s ChatEvent branch still links every
`e` tag to the parent. That link is what populates `note.replies`, which the
thread and the chip read — discriminating there would unlink Buzz replies from
their parents. The marker distinction belongs in rendering, not in linkage.

Verified on device against a real thread: the reply now sits in the thread on
"howd you get bumble working?" with a "3 replies" chip on the parent, and is
gone from the channel timeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:14:15 -04:00
davotoula 482be60e4a Code review:
- fold audioExt into videoExt and share the classifier
2026-07-26 20:26:52 +02:00