Commit Graph
17078 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub 17e45d4e68 Merge pull request #3600 from vitorpamplona/claude/amethyst-note-ui-refactor-n36nvd
refactor: converge rich-text rendering across Android + Desktop into commons
2026-07-16 19:49:51 -04:00
Vitor PamplonaandGitHub 0d219e219c Merge pull request #3602 from vitorpamplona/claude/invite-not-working-x1mhrf
Improve Concord invite error messaging and Gradle infra detection
2026-07-16 19:44:05 -04:00
Claude 0fa1a26c26 fix(amethyst): restore no-preview suppression of invoice/withdraw UI
Audit of the rich-text convergence found a behavior regression: the Android
RichTextSegmentRenderer.Payment ignored canPreview and always rendered the
interactive MayBeInvoicePreview / MayBeWithdrawal UI. The original
RenderWordWithoutPreview switchboard deliberately showed plain text for
invoice and withdraw segments when previews are suppressed ("Don't offer to
pay invoices" / "Don't offer to withdraw"). Branch on canPreview for those two;
Cashu and Clink stay unconditional (they decode locally, network only on tap),
matching the original.

Verified: :amethyst play debug compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 23:42:26 +00:00
Claude a2ee1880a6 fix(hooks): treat Gradle distribution download failure as infra skip
The pre-push spotless gate already skips itself (warn + exit 0) when
`./gradlew spotlessApply` fails for infra reasons, so a restricted sandbox
can't strand a push over formatting. But its detection regex only covered
dependency-resolution/network errors, not the case where the Gradle wrapper
can't download the pinned distribution itself — e.g. an egress-proxy 40x on
`gradle-<ver>-bin.zip`. That surfaced as a hard BLOCK on an XML-only change
even though CI's spotlessCheck would pass.

Add the distribution-bootstrap markers (the `gradle-<ver>-bin.zip` URL, the
Java `Server returned HTTP response code` download exception, and "could not
install gradle") to the infra-skip regex. These strings only appear when
gradlew failed before running any task, so they can't mask a real
formatting/compile failure (verified: a spotlessKotlinCheck violation still
blocks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157AWJNEzybTuegstbWiQ6B
2026-07-16 23:38:14 +00:00
Claude 9eb3b75615 fix(concord): clarify invite failure copy for token/decrypt mismatch
concord_invite_failed_incompatible is shown for the Unreadable status,
which fires both for a genuinely newer bundle format AND for the far more
common case where the fragment's unlock token does not decrypt the
published kind-33301 bundle (a stale link, or a coordinate re-posted under
a new token). The old copy asserted "created with a newer version of the
app", which is usually wrong, and suggested "try again after updating"
even though this state is non-retryable.

Reword to lead with the likely cause (outdated/replaced link), keep the
newer-version case as a secondary possibility, and point the user at
getting a fresh invite link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157AWJNEzybTuegstbWiQ6B
2026-07-16 23:33:30 +00:00
Claude c90353d944 refactor(amethyst): adopt the shared ClickableUrl for plain links
Amethyst's ClickableUrl now delegates plain http(s) links to the shared
commons ClickableUrl (one LocalUriHandler-based implementation for both front
ends); only the Android-specific blossom: case (opening a Blossom media URI via
an Intent) stays local. The public signature is unchanged, so all 12 call sites
are untouched.

Verified: :amethyst play debug compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 22:43:58 +00:00
Claude 19f5065af4 refactor(commons): shared ClickableUrl/ClickableEmail; Desktop drops its duplicate
Desktop's ClickableLink and Amethyst's ClickableUrl were near-identical: the only
real differences are mouse-first styling (Desktop underlines + shows a hand
cursor) and the open mechanism. But LocalUriHandler.openUri opens the browser on
Android AND Desktop (and the mail client for mailto:), so the "open a link"
logic never needed to be platform-specific.

Add ClickableUrl/ClickableEmail to commons/ui/components on LocalUriHandler, with
an `underline` flag so each front end keeps its exact look. Desktop's rich-text
renderer now reuses them (underline = true) for url/email/link-preview/withdraw
and its bespoke ClickableLink is deleted. Amethyst keeps its own blossom-intent-
aware ClickableUrl (blossom never applies to plain links, so this path is
equivalent); Phone stays platform-specific (Android dials, Desktop has no dialer).

Verified: :commons JVM and :desktopApp compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 22:24:27 +00:00
Claude 9eb827fed6 feat(commons): route url/email/phone through the platform strategy
Closes the last rich-text fidelity residual. url/email/phone were rendered
generically by the shared core via RichTextInteractions callbacks, losing each
front end's per-type styling and open behavior. They now go through the
RichTextSegmentRenderer strategy:
- Add Url(url, displayText)/Email(address)/Phone(number) to the contract (with
  plain-text defaults).
- Core routes LinkSegment(no-preview)/SchemelessUrl -> Url, Email -> Email,
  Phone -> Phone; drop the in-core ClickableSpan.
- Amethyst renders them with ClickableUrl/ClickableEmail/ClickablePhone (blossom
  intent + dial preserved); Desktop with ClickableLink / underlined mailto / plain
  phone text.
- RichTextInteractions now carries only onClickHashtag (the one segment the core
  draws itself, with shared icons); the onOpen* callbacks are gone.

Verified: :commons JVM, :desktopApp, :amethyst play debug compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 22:12:28 +00:00
Claude 51f005d22f refactor(commons): unify the rich-text parser cache across Android + Desktop
Replaces the two forked cached parsers (amethyst CachedRichTextParser on
android.util.LruCache + desktop DesktopCachedRichTextParser on ConcurrentLruCache
with a naive isMarkdown) with one shared object in commons/jvmAndroid/richtext,
built on quartz ConcurrentLruCache and keeping amethyst's CommonMark-aware
computeIsMarkdown and content-addressed key (content+tags+callbackUri+authorPubKey).

- Add ConcurrentLruCache.trimToSize(maxItems) (+ tests) for the onTrimMemory path.
- Repoint all amethyst callers (incl. the markdown unit test) and both desktop
  callers; delete both forks.

Verified: :commons JVM, :desktopApp, :amethyst play debug + unit tests compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 21:56:12 +00:00
Claude 269ae95b4b feat(desktop): render rich text through the shared core; delete the Desktop fork
Implements the cross-platform RichTextSegmentRenderer contract on Desktop
(mouse-first) and converges Desktop onto the shared commons RichTextViewer:
- DesktopRichTextSegmentRenderer draws each divergent segment with the existing
  Desktop leaves (AsyncImage media + onImageClick, RenderInvoiceCard/RenderCashuCard,
  QuotedNoteEmbed, RenderBechSegment, RenderPdfCard/RenderNowhereLinkCard,
  RenderSecretEmoji, relay copy).
- DesktopRichText replaces the hand-rolled DesktopRichTextViewer switchboard: it
  parses with DesktopCachedRichTextParser, keeps markdown on RenderMarkdown, and
  drives the shared core via the two CompositionLocals. NoteCard repointed.
- Delete the old DesktopRichTextViewer + RenderSegment + the duplicate
  RenderCustomEmojiSegment (the core now renders emoji); rename the file.
- Add a NowhereLink method to the contract so both platforms keep their
  nowhere.ink card (the core no longer flattens it to a plain link); Android
  adapter implements it via NowhereLinkCard.

Verified: :commons JVM, :desktopApp, and :amethyst play debug all compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 21:43:56 +00:00
Vitor PamplonaandGitHub 6d9a7e8258 Merge pull request #3599 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-16 17:32:42 -04:00
Claude 435ad29f11 feat(commons): share hashtag icons + emoji; route Android RichTextViewer through the shared core
Closes the fidelity gap and flips Amethyst's rich text onto the shared core:
- Move the hashtag-icon table (HashtagIcon + checkForHashtagWithIcon) into
  commons/ui/richtext (the icons were already in commons); amethyst re-exports
  it for existing call sites.
- Add a commons custom-emoji renderer (RenderCustomEmoji + InLineIconRenderer)
  built on quartz CustomEmoji.assembleAnnotatedList, mirroring CreateTextWithEmoji.
- Wire both into the commons RichTextViewer core (hashtags now show shared inline
  icons; emoji render inline).
- Point amethyst's RichTextViewer at CommonsBackedRichTextViewer (the shared core
  is now the production plain-text path; ~55 call sites unchanged) and delete the
  now-dead private RenderRegular switchboard. Markdown stays native.

Verified: :commons JVM and :amethyst play debug compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 21:30:54 +00:00
Claude 834dfc9fc6 feat(amethyst): Android adapter for the shared rich-text contract
Implements the commons RichTextSegmentRenderer with Amethyst's existing
touch-first leaves (ZoomableContentView, ImageGallery, LatexEquation,
BechLink/TagLink, MayBe{Invoice,Withdrawal}/CashuPreview/ClinkOffer,
LoadUrlPreview, relay/concord chips, DisplaySecretEmoji), closing over
AccountViewModel/INav/backgroundColor/callbackUri/canPreview.

Adds CommonsBackedRichTextViewer, a parallel entry that parses with the
existing CachedRichTextParser, keeps markdown native, and drives the
shared commons RichTextViewer core via the two CompositionLocals. Left
parallel to the existing RichTextViewer so the core is exercised against
real leaves without touching the ~55 call sites; flipping the entry over
(and deleting the duplicated switchboard) is the reviewed follow-up.

Verified: :commons JVM target and :amethyst play debug both compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 21:10:09 +00:00
Claude c8deb0f227 feat(commons): prototype shared rich-text rendering contract
Introduces commons/ui/richtext: one cross-platform RichTextViewer that
both the touch (Amethyst Android) and mouse-first (Desktop) front ends
can drive, to replace the two current forks (amethyst RichTextViewer +
DesktopRichTextViewer).

The shared core owns the universal parts (paragraph/RTL/word layout,
plain text, inline custom emoji, hashtags) and delegates the segments
whose *presentation and* call-to-action diverge by platform (media,
equation, quoted event, mention, payment, link preview, relay/invite,
secret message) to a RichTextSegmentRenderer strategy provided via
LocalRichTextSegmentRenderer -- the same CompositionLocal idiom the
codebase already uses for LocalInlineQuoteRenderer. Universal actions
(open url/email/phone, hashtag) go through a small RichTextInteractions
callback bag. A PlainTextSegmentRenderer default keeps the core usable
from previews/tests/headless callers.

Contract + skeleton only; compiles in :commons. No consumer wired yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 20:45:58 +00:00
vitorpamplonaandgithub-actions[bot] 5c19017ef6 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-16 19:27:09 +00:00
Vitor PamplonaandGitHub 9291c9770a Merge pull request #3597 from vitorpamplona/claude/limits-message-support-g5plrd
Add NIP-22 LIMITS message support with per-relay tracking
2026-07-16 15:24:50 -04:00
Claude a068eaf721 docs(commons): add rich-text restructure as gating Phase 0
TranslatableRichTextViewer can't move (ML-Kit translation is Android),
but the stack is already layered: translation wraps ExpandableRichText
wraps the 1031-LOC RichTextViewer core. TranslationConfig state is
already in commons and the core's real AccountViewModel surface is 5
members (3 cache reads -> ICacheProvider, toast -> callback, nav ->
callbacks). Move the core to commons/ui/text behind the cache port +
callbacks with a renderEmbeddedNote slot for the NoteCompose recursion;
keep the translation wrapper native and thin. This gates Tier 2 so
nested rich text becomes a direct commons call, not a per-renderer slot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 18:49:57 +00:00
Claude ccecc5edd4 style: spotlessApply on LocalPreferences after merging main
Merged origin/main into the branch; a formatting violation in
LocalPreferences.kt (globalSettingsPrefs, added on main) was surfacing in
the PR merge-ref lint check. spotlessApply collapses it to one line
(whitespace only, no behavior change) so CI's lint job passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ
2026-07-16 18:49:12 +00:00
Claude 2a3a4437ca Merge remote-tracking branch 'origin/main' into claude/limits-message-support-g5plrd 2026-07-16 18:48:35 +00:00
Claude 0f07d63d7a docs(commons): read note lookups through the cache port, not lambdas
Account already injects `val cache: LocalCache` and LocalCache implements
the commons `ICacheProvider`/`ILocalCache` read ports, but AccountViewModel
lookups bypass account.cache and hit the object singleton directly, and
IAccount doesn't expose the cache. Revise the extraction seam: reads cross
via `IAccount.cache: ICacheProvider` (add it) rather than app-supplied
loader lambdas; reserve lambdas for nav + write/signer actions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 18:47:31 +00:00
Vitor PamplonaandGitHub 5f6fde339a Merge pull request #3598 from vitorpamplona/claude/pull-notification-toggle-ezy8l6
Add global master switch for always-on notification service
2026-07-16 14:39:48 -04:00
Claude 654dd2b1b3 docs(commons): review plan for extracting ui.note rendering to commons
Study of com.vitorpamplona.amethyst.ui.note (214 files / ~48.7k LOC,
types/ = 89 files) for moving the rendering half into commons via a
Render (entry, stays native) -> Display (pure, moves to commons) split.
Documents the canonical entry signature, the AccountViewModel/INav/
R.string/leaf-toolkit dependency surface, the @Composable-slot seam for
the flavor-specific rich-text viewer, a tiered categorization of the 89
type renderers, and a per-event sequencing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
2026-07-16 18:39:05 +00:00
Claude 452a1c8186 fix: harden LIMITS parsing and dedup, per audit
Addresses the four findings from the branch audit:

- Make the kotlinx codec (LimitsKSerializer, the iOS/native incoming path)
  as lenient as the Jackson path: mistyped fields degrade to null, non-int
  array elements are skipped, and a payload-less ["LIMITS"] frame yields an
  empty message instead of throwing. Guard the payload access in
  MessageKSerializer and MessageDeserializer likewise.
- Make the Jackson reads (LimitsDeserializer) type-checked so an explicit
  JSON null or wrong-typed value stays null ("unspecified — keep previous")
  instead of coercing to false/0. Both codecs now behave identically.
- LimitsMessage -> data class, so StateFlow.distinctUntilChanged in
  RelayLimitsTracker suppresses no-op emissions when a relay re-advertises
  identical limits, and tests get value equality.
- Drop the stale "NIP-22" labels (LIMITS is nostr-protocol/nips#1434, not
  NIP-22) from LimitsKSerializer, MessageKSerializer and MessageSerializer.

Tests: added mistyped/null-field and payload-less coverage on both the
Jackson and kotlinx paths, plus a value-equality check. quartz:jvmTest
(RelayWireErgonomicsTest 11, KotlinSerializationMapperTest 57,
RelayLimitsTrackerTest 5) and amethyst play compile green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ
2026-07-16 18:37:35 +00:00
Claude ae81c93c04 feat: global background-notification-service master + per-account participation
Redesigns the always-on notification control into two switches:

- A global master ("Background notification service") — a persisted, battery-saver
  "airplane mode" stored in global preferences that overrides every account. The
  Quick Settings tile toggles this; it survives restarts and crashes until re-enabled.
- Per-account participation ("Keep this account active in the background") — the
  existing per-account setting, now surfaced as a per-write-account list under the
  master in the Notification Settings screen.

The always-on service runs while the master is on AND at least one writable account
participates; turning the master off tears down every layer for all accounts. The
manager reacts to both the master flow and each loaded account's participation flag,
and the restart layers (watchdog/boot/worker) honor the same rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKLqvcC1vobUYs3PxL2zWU
2026-07-16 18:37:28 +00:00
Claude 9b7aca77a6 docs: add relay LIMITS status + roadmap plan
Captures what's done (parse, RelayLimitsTracker, cleanup), the audit fixes
to fold in (kotlinx parse leniency to match Jackson on iOS, drop stale
NIP-22 labels, make LimitsMessage a data class), and the remaining roadmap
(client apply helpers, server-side emit, NIP-11 bridge, amy tooling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ
2026-07-16 18:28:27 +00:00
Claude 041abf9e37 refactor: reconcile duplicate LIMITS code and rename client accessory
Cleanup after adding LimitsMessage + the client-side cache:

- Remove the unused experimental LIMITS prototype
  (experimental/limits/Limits.kt + LimitProcessor.kt). Its @Serializable
  model duplicated LimitsMessage (minus auth_for_read/auth_for_write and
  the Message wiring); the processor's clamp/reject logic is superseded by
  the server-side LimitsPolicy and will be reincarnated as pure helpers on
  LimitsMessage. Both were prototypes with no references (preserved in git
  history).
- Rename the client accessory RelayLimits -> RelayLimitsTracker so it no
  longer collides on simple name with the relay-server-side
  nip01Core.relay.server.policies.RelayLimits (the operator-configured
  limits a relay enforces and advertises). Updates AppModules and the test.

No behavior change. quartz:jvmTest (RelayLimitsTrackerTest 5/5) and
amethyst play-flavor compile are green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ
2026-07-16 17:21:00 +00:00
Claude 9daa0b3e48 feat: cache and expose relay LIMITS per relay
Adds RelayLimits, a passive connection-listener accessory (modeled on
RelayAuthenticator) that caches the latest LIMITS each relay advertises
and publishes it as a Compose-stable StateFlow, so consumers can read a
relay's current rights/limits instead of only observing the raw message.

- RelayLimits: caches LimitsMessage per NormalizedRelayUrl, exposes
  limitsFlow (StateFlow), get(url) and snapshot(); connection-scoped
  (entry dropped on disconnect so stale limits don't leak).
- LimitsMessage marked @Immutable for Compose stability in the flow map.
- Wired into AppModules next to relayStats (Amethyst.instance.relayLimits).
- Tests: per-relay caching, later-replaces-earlier, independent relays,
  drop-on-disconnect, and ignore non-LIMITS messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ
2026-07-16 15:39:04 +00:00
Claude 94c9dc7f77 feat: parse relay LIMITS message (NIP relay limits)
Adds support for the relay-to-client LIMITS frame, which advertises the
connection's current rights and limits (can_read/can_write, auth
requirements, max_message_length, max_subscriptions, max_filters,
max_limit, POW, rate limits, required tags, etc.). Relays such as
wss://pipe.imwald.eu/ send it on connect and whenever rights change,
and Amethyst previously logged it as an unsupported message.

- New LimitsMessage model in quartz commonMain with every optional field.
- Jackson decode/encode (LimitsDeserializer + MessageSerializer) and the
  kotlinx-serialization path (LimitsKSerializer + MessageKSerializer).
- Logs a summary line in RelayLogger.
- Tests: production payload parse, array/tag fields, empty object, and
  round-trip parity between the Jackson and kotlinx codecs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ
2026-07-16 15:23:25 +00:00
Vitor Pamplona 7143d81e15 Activating parallel sync from the new AGP 2026-07-16 10:03:30 -04:00
Vitor PamplonaandGitHub 5085bf3f04 Merge pull request #3595 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-16 09:50:37 -04:00
vitorpamplonaandgithub-actions[bot] 9d0cc8ad56 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-16 13:40:26 +00:00
Vitor PamplonaandGitHub 829994076f Merge pull request #3596 from vitorpamplona/claude/nip-29-compliance-review-ngzzaw
NIP-29: subgroups, custom roles, timeline refs, invite links + relay-signed hardening
2026-07-16 09:37:28 -04:00
Claude 37220306e9 fix(nip29): reject non-relay-signed group state; share one-tap invite links
Authorization hardening:
- LocalCache only applies a kind 39000-39005 addressable (group metadata,
  admins, members, roles, pins) to a group's state when the event is signed by
  the relay's own NIP-11 `self` key. Previously any author's 39001 served for a
  group id was applied (newest wins), so a stray/malicious user-published admin
  list on a lax relay could inject itself as admin in the client's view and
  unlock moderation UI. The guard only blocks when `self` is known and differs,
  so legitimate groups (and relays whose NIP-11 hasn't loaded) are unaffected.
  Moderation events (9000-9010) were already stored-but-not-applied — the client
  relies on the relay-republished 39001/39002 — so they need no change.

Invite links:
- The invite dialog now produces the spec's single `naddr1…?invite=<code>`
  link for closed groups and copies just that, so one tap joins. It previously
  copied the naddr and code as two separate lines, which the new parser (that
  reads `?invite=`) could never round-trip.

spotless clean; amethyst compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qst2JsmNYMvXitv2vxo4S
2026-07-16 13:18:51 +00:00
Vitor PamplonaandGitHub 3a112ed173 Merge pull request #3594 from nrobi144/fix/desktop-auth-tier1-coldboot-race
fix(desktop): auto-approve pending relay AUTH once the DM-inbox (kind:10050) set loads
2026-07-16 07:05:06 -04:00
nrobi144andClaude Opus 4.8 d1ed9d071b fix(desktop): auto-approve pending AUTH once the DM-inbox relay set loads
On cold boot an AUTH-required kind:10050 DM-inbox relay usually sends its
AUTH challenge before the account's own kind:10050 list has been fetched.
AuthApprovalPolicy.classify reads the trusted (self-approved) relay set
exactly once, so it classifies the user's OWN inbox relay as tier-2 and
surfaces a manual `[Once][Always][Never]` banner. Nothing re-evaluates
that pending decision when the kind:10050 list finally loads, so the user
gets a spurious AUTH prompt for a relay that should have auto-signed.

Extract the pending-approval set into a platform-agnostic
commons/AuthApprovalRequests (add/resolve/cancelAll) and add
autoApproveNowTrusted(): when the DM-inbox set updates, retroactively
settle every pending prompt whose relay is now tier-1 with ONCE — sign
this session, do not persist (trusted by identity, not an explicit grant).

DesktopAuthCoordinator now delegates its pending set to AuthApprovalRequests
and exposes onSelfApprovedRelaysChanged(); Main.kt drives it from
DesktopAccountRelays.dmRelayList (the account's kind:10050 StateFlow).

AuthApprovalRequestsTest reproduces the cold-boot race (red before the fix)
and covers the resolve / cancelAll paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:31:18 +03:00
Claude 0e8239b22f fix(nip29): audit fixes — subgroup edit safety, subscription load, roles
Bugs:
- Metadata edit could re-root a subgroup or drop its children on a load race:
  the edit ViewModel snapshotted parent/children at prefill and overrode the
  Account-level live-read defaults. Children are no longer snapshotted (Account
  reads the live child list at save time), and the parent is only overridden
  when the user actually re-parents (parentTouched) — a plain rename can't
  re-root or orphan children anymore, even if metadata hadn't loaded yet.
- Parent selector card cached a null channel via remember() and never
  refreshed, so the parent's name/picture never loaded and the warm-up never
  mounted. Now get-or-create + warm + observe the metadata flow.
- previousEventRefs could let a note with an unresolved author slip past the
  self-exclusion and reference the sender's own event. Now requires a resolved
  author.
- Assigning a relay-defined role replaced the member's whole role set while the
  menu implied additive; now keeps existing roles (entry.roles + role.name).
- GroupNAddrInvite now also accepts a bare `invite=<code>` remainder if the `?`
  is stripped upstream (+ test).

Performance:
- Subgroups bar mounted a full warm-up (metadata + content) subscription per
  child chip — up to ~21 relay subscriptions per open group. Replaced with one
  relay-directory subscription; chips read from cache.
- Parent picker recomputed the whole candidate scan on every recomposition
  (each search keystroke) via a produceState initial-value argument; the scan
  now lives only in the producer with a cheap empty initial.

spotless clean; quartz tests green; amethyst compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qst2JsmNYMvXitv2vxo4S
2026-07-16 04:19:25 +00:00
Claude c280c14b3c feat(nip29): subgroup authoring — parent-group picker in the edit form
Lets an admin nest a group under a parent (or promote it back to top-level)
from the create/edit metadata form, completing the subgroup feature's authoring
side (viewing/navigation already shipped).

- RelayGroupMetadataViewModel: editable parentGroupId + preserved children,
  seeded from the group's metadata and passed through on save. NIP-29 requires a
  kind-9002 to re-list every child, so children are carried untouched.
- Account.createRelayGroup: optional parent so a group can be created nested.
- RelayGroupParentPicker: a hero selector card (gradient badge, parent avatar or
  a top-level/home glyph) that opens a modal bottom-sheet picker — search field,
  a "no parent (top-level)" choice, and the relay's genuine groups with avatars,
  member counts and an animated selection mark. Excludes the group itself and its
  own descendants so a cycle can't be formed, and streams the relay directory
  while open so candidates fill in live.

spotless clean; amethyst compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qst2JsmNYMvXitv2vxo4S
2026-07-16 02:45:58 +00:00
Claude c236474f88 feat(nip29): timeline refs, custom roles, subgroup nav, naddr invites
Follows up the subgroup protocol work with four NIP-29 compliance/UX gaps.

previous timeline references (spec §Timeline references):
- RelayGroupChannel.previousEventRefs(): first-8-char id prefixes of the most
  recent events seen from the host relay, excluding the sender's own, capped at
  the spec's 50-event window. Only draws from events actually received in the
  channel so the host relay is known to have them.
- Populate the `previous` tag on all outgoing group events: kind-9 chat and
  replies, kind-1111 minichat comments, kind-11 threads, and group replies.

Custom roles (kind 39003):
- Route SupportedRolesEvent onto the channel (LocalCache.consume + a
  RelayGroupChannel.supportedRoles field) instead of only storing it.
- Members screen: when the relay advertises a role set, offer those roles when
  assigning (admins), and show each member's real relay-assigned role label
  instead of collapsing everything to admin/moderator. Falls back to the
  built-in admin/moderator shortcuts when no 39003 is published.

Subgroup navigation (spec §Subgroups):
- RelayGroupSubgroupsBar: a self-hiding bar under the pinned bar showing a
  breadcrumb up to the parent group and chips for the child subgroups (in the
  relay's `child` order), each opening that group on the same host relay.

naddr invite codes (spec §Group identifier):
- GroupNAddrInvite parses the `naddr1…?invite=<code>` suffix; both the tap
  handler (ClickableRoute) and the deep-link handler (MainActivity) now feed it
  into the kind-9021 join request so a shared invite naddr auto-joins.

Tests for the naddr invite parser. spotless clean; quartz tests green;
amethyst compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qst2JsmNYMvXitv2vxo4S
2026-07-16 02:29:10 +00:00
Vitor PamplonaandGitHub 99f28ee1ca Merge pull request #3593 from vitorpamplona/claude/nip-51-compliance-review-vwhyvn
Add hashtag muting support via NIP-51
2026-07-15 22:07:00 -04:00
Claude bea342a731 chore: remove dead kind:10011 GalleryListEvent
GalleryListEvent (kind 10011, @Deprecated "Replaced by NIP-68") was
unreachable: EventFactory routes kind 10011 to ExternalIdentitiesEvent,
and no other code referenced the class. It also shadowed NIP-51's
"Favorite follow sets" kind. Removing the dead class; the live profile
gallery entry (ProfileGalleryEntryEvent) and its builders are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017giudm3gXumsxZmd3uMQc8
2026-07-16 02:00:44 +00:00
Vitor PamplonaandGitHub c39ebc5c93 Merge pull request #3591 from vitorpamplona/claude/relay-auth-request-repeat-0frbqp
Fix AUTH grant persistence for long relay URLs
2026-07-15 21:58:54 -04:00
Claude abc6732286 feat: support NIP-51 mute-list hashtag ("t") entries
NIP-51's kind:10000 mute list defines four entry types — `p` (pubkeys),
`word`, `e` (threads) and `t` (hashtags). Quartz parsed only the first
three, so `t` hashtag mutes written by other clients were silently
dropped: uncounted, invisible, and never applied to filtering.

Quartz:
- Add HashtagTag (`"t"`) implementing the MuteTag sealed interface, and
  register it in MuteTag.parse/isTagged so it round-trips like the other
  entry types.
- Add mutedHashtags()/mutedHashtagIds() TagArray helpers.

Filtering (commons):
- Add hiddenHashtags to LiveHiddenUsers plus isHashtagHidden(), and hide
  notes carrying a muted hashtag in Note.isHiddenFor() (exact, case-
  insensitive `t`-tag match — distinct from the existing substring word
  scan).

Amethyst:
- Aggregate HashtagTag entries from the mute/block lists in
  HiddenUsersState.
- MuteListState.hideHashtag/showHashtag + Account and AccountViewModel
  wrappers, and observeUserIsMutingHashtag.
- Surface a Mute/Unmute hashtag action in the hashtag screen's options
  overflow menu.

Tests: HashtagTagTest (parse/round-trip/MuteTag dispatch) and
NoteIsHiddenForTest cases for muted-hashtag hiding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017giudm3gXumsxZmd3uMQc8
2026-07-16 01:51:48 +00:00
Claude e24eec9ca6 feat(nip29): subgroup hierarchy (parent/child) for relay groups
Implements the NIP-29 Subgroups feature merged upstream: groups can now be
organized into a parent/child tree, scoped per host relay.

Quartz:
- Add `parent`/`child` tag classes and TagArray (builder) helpers.
- GroupMetadataEvent (39000): parent()/children()/isRoot() accessors and
  build params.
- EditMetadataEvent (9002): parent()/children() accessors and build params
  (a 9002 re-carries the full child list, per spec, or the relay rejects it).
- SubgroupTree: assembles a relay's flat 39000 set into the hierarchy —
  structure follows each group's parent tag, sibling order follows the
  parent's child-tag order, orphans surface as roots, and malformed cycles
  are broken rather than looping.
- NIP-11: advertise/detect subgroup support via `nip29: { subgroups: true }`,
  with a `subgroups()` builder DSL helper.
- Tests for tag round-trips, tree assembly, ordering, orphans and cycles,
  plus NIP-11 serialization.

Amethyst:
- RelayGroupChannel: parentGroupId()/childGroupIds()/isSubgroup() reading the
  latest metadata.
- Account.editRelayGroupMetadata: preserve the group's current parent and full
  children list on a plain metadata edit so an admin renaming a subgroup no
  longer detaches it (or gets rejected for dropping children).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qst2JsmNYMvXitv2vxo4S
2026-07-16 01:51:40 +00:00
Claude 72a70ab635 test: make AUTH approval key test OS-independent
The previous regression test drove PreferencesAuthApprovalStore against
the real java.util.prefs.Preferences.userRoot() backing store, which
threw IllegalArgumentException on the macOS CI runner (its preferences
backend rejects operations the Linux one accepts).

Extract the key derivation into a pure `authApprovalPreferenceKey`
function and assert the invariant that actually matters — every derived
key stays within Preferences.MAX_KEY_LENGTH, which is exactly the
condition put() enforces. No OS I/O, so it runs deterministically on
every platform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015aJPf1AsPTdzdMYR6pZ4e2
2026-07-16 01:38:54 +00:00
Vitor PamplonaandGitHub c0d816ad86 Merge pull request #3590 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-15 21:34:49 -04:00
vitorpamplonaandgithub-actions[bot] 01fbc34e5e chore: sync Crowdin translations and seed translator npub placeholders 2026-07-16 01:32:20 +00:00
Vitor PamplonaandGitHub 67efac7972 Merge pull request #3592 from vitorpamplona/claude/amethyst-invite-link-hang-w0tagl
CORD-05: Distinguish invite redemption failures for better UX
2026-07-15 21:29:52 -04:00
Claude b3fce19ff7 fix: persist desktop AUTH approvals for long relay URLs
The desktop AUTH banner ("<relay> requires authentication to deliver
this message") kept re-appearing on every challenge even after the user
clicked Always or Never.

Root cause: PreferencesAuthApprovalStore used the raw relay URL as the
java.util.prefs.Preferences key. Preferences caps keys at
MAX_KEY_LENGTH (80 chars) and throws IllegalArgumentException from put()
for anything longer. Outbox-proxy relay URLs that embed an npub and a
query string routinely exceed that (e.g.
wss://filter.nostr.wine/npub1...?broadcast=true is 102 chars), so
setScope threw. RelayAuthenticator swallows the exception, so the
ALWAYS/BLOCKED grant was silently never persisted and the relay
re-prompted on the next AUTH challenge.

Fold any relay URL over MAX_KEY_LENGTH into a bounded sha256:-prefixed
64-char hex key. Short URLs are still stored verbatim so existing grants
keep working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015aJPf1AsPTdzdMYR6pZ4e2
2026-07-16 01:25:34 +00:00
Claude ae9f4a0def fix(concord): resolve invite coordinate per CORD-05 (honor revocation)
Follow-up making the invite redeemer match the CORD-05 §2 spec for the
addressable invite coordinate (33301, link_signer, d=""):

- vsk=6 → live bundle (open with the link token)
- vsk=9 → revocation tombstone: the newest event wins, so a tombstone buries
  even a stale, still-openable copy on another relay ("a fetcher finds the
  grave instead of keys"). Amethyst previously never checked for this, so a
  revoked link failed generically.
- anything else present (e.g. a mis-posted registry vsk=8, the shape of the
  relayop.xyz link that hung) → unreadable
- nothing on any relay → absent

New pure `ConcordInviteBundle.classify(wraps, token): InviteBundleStatus` in
quartz (next to parse/validate), wrapped by `ConcordActions.classifyInvite`,
and mapped by `Account.joinConcordViaInvite` to the `ConcordInviteResult`
cases — including a new `Revoked` outcome with its own message and no futile
retry. Crypto is unchanged and already matches the spec
(hkdf(token,'concord/invite-key') → NIP-44 → snake_case CommunityInvite).

Adds ConcordInviteClassifyTest covering live / revoked (order-independent) /
unreadable / absent, plus the real relayop.xyz vsk=8 event → Unreadable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KngFNwrQDLYa9QW1f5RRD
2026-07-16 01:24:37 +00:00
Vitor PamplonaandGitHub 615aa221e1 Merge pull request #3589 from vitorpamplona/claude/coroutine-dispatcher-access-fix-42chwt
Add IO dispatcher import to BlossomServerListState
2026-07-15 21:14:38 -04:00