Commit Graph
100 Commits
Author SHA1 Message Date
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
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
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
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
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
Vitor PamplonaandGitHub c0d816ad86 Merge pull request #3590 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-15 21:34:49 -04: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
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
Vitor PamplonaandGitHub f586656223 Merge pull request #3585 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-15 20:36:56 -04:00
Vitor PamplonaandGitHub a4931ce295 Merge pull request #3587 from vitorpamplona/claude/nip17-dm-relay-desktop-4shxdy
Desktop NIP-17: deliver DMs & reactions to the recipient's DM relays, downloaded up front
2026-07-15 20:36:42 -04:00
Vitor PamplonaandGitHub c95270fca3 Merge pull request #3583 from vitorpamplona/claude/blossom-server-loading-sync-n1l3sw
Sync Blossom media server list (kind 10063) across clients
2026-07-15 20:11:48 -04:00
Vitor PamplonaandGitHub 13cbcf41d6 Merge pull request #3586 from vitorpamplona/claude/nip29-message-pinning-elzck9
NIP-29 message pinning: UI bar, moderation events, and relay group integration
2026-07-15 19:56:53 -04:00
Vitor PamplonaandGitHub 6a28057890 Merge pull request #3584 from vitorpamplona/claude/chatcompose-timeago-padding-v0yypr
Skip empty reaction chip rows on messages without engagement
2026-07-15 19:36:31 -04:00
Vitor PamplonaandGitHub a906d4ee58 Merge pull request #3580 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-15 19:35:35 -04:00
Vitor PamplonaandGitHub f98799540b Merge pull request #3582 from vitorpamplona/claude/channel-list-enhancements-wktw8t
Concord: richer channel rows — previews, unread counts, facepiles, live typing
2026-07-15 19:35:23 -04:00
Vitor PamplonaandClaude Opus 4.8 18f1d87492 fix: Concord unread badge + last-message previews ignore thread replies
The unread count, the channel-list/hub last-message previews, and the
Messages-row summary all read a broader note set than the channel feed
renders: the feed's ChannelFeedFilter hides kind-1111 CommentEvent thread
replies (and unacceptable authors), but these row surfaces counted/showed
them. A trailing minichat reply therefore stuck the unread badge at a count
opening the channel could never clear (markAsRead is monotonic and only
advances for rendered timeline messages), and showed up as a "last message"
that isn't on the timeline.

Centralizes the feed's predicate as isConcordTimelineMessage (loaded,
acceptable, not a CommentEvent) plus a newestTimelineNote helper, and routes
the unread count, both list-row previews, and the Messages hub filter through
it — so every channel-row summary agrees with what the open channel renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:24:45 -04:00
Vitor PamplonaandGitHub 2be4929a97 Merge pull request #3581 from vitorpamplona/claude/kotlin-coroutines-opt-in-t940p3
Fix reactive state handling in Concord entry display
2026-07-15 17:23:21 -04:00
Vitor PamplonaandGitHub 000d01c44e Merge pull request #3579 from vitorpamplona/claude/modernize-chat-rendering-kigcsh
Modernize chat message rendering
2026-07-15 16:27:22 -04:00
Vitor PamplonaandGitHub d37e88f620 Merge pull request #3574 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-15 16:24:43 -04:00
Vitor PamplonaandGitHub efa74169b7 Merge pull request #3578 from vitorpamplona/claude/nip29-group-preloading-18kc3e
Prefetch recent group chat messages for list previews
2026-07-15 16:08:19 -04:00
Vitor PamplonaandGitHub 48358603bc Merge pull request #3577 from vitorpamplona/claude/concord-image-display-issue-6qba0w
Extract imeta URL rendering logic to shared utility
2026-07-15 16:06:05 -04:00
Vitor PamplonaandGitHub 21b98b3ebf Merge pull request #3576 from vitorpamplona/claude/bottom-nav-item-selection-xt85y9
Bottom nav: pin individual chats/groups + redesigned setup screen
2026-07-15 14:51:19 -04:00
Vitor PamplonaandGitHub bfd8ef406c Merge pull request #3575 from vitorpamplona/claude/messages-fab-group-selection-mkddvi
Replace Messages FAB speed-dial with full-screen conversation chooser
2026-07-15 12:01:57 -04:00
Vitor PamplonaandGitHub 7743f36ac6 Merge pull request #3573 from vitorpamplona/claude/concord-image-display-ou01vs
Support NIP-92 imeta attachments in chat messages
2026-07-15 11:11:41 -04:00
Vitor PamplonaandGitHub 7de5e75856 Merge pull request #3572 from vitorpamplona/claude/kotlin-compiler-warnings-g10py4
Replace nullable safe calls with immutable map operations
2026-07-15 10:48:30 -04:00
Vitor PamplonaandGitHub c0f95a5139 Merge pull request #3571 from vitorpamplona/fix/strictmode-loopback-cleartext
fix(strictmode): stop CleartextNetworkViolation noise from the Tor SOCKS proxy
2026-07-15 10:41:37 -04:00
Vitor PamplonaandClaude Opus 4.8 ac60b294ed fix(strictmode): stop CleartextNetworkViolation noise from the Tor SOCKS proxy
detectCleartextNetwork() is enforced by netd per-UID at the packet level: it
flags any socket whose first bytes aren't a TLS handshake, with no per-host
exemption. Every relay WebSocket tunneled over the embedded Arti Tor SOCKS
proxy on 127.0.0.1:17392 opens with a cleartext SOCKS5 greeting, so the
detector fired constantly on legitimate loopback traffic.

The Network-Security-Config localhost allowlist does NOT silence it — that flag
only governs the voluntary NetworkSecurityPolicy.isCleartextTrafficPermitted()
check that HTTP stacks consult, not netd's packet inspection. The old XML
comment claiming otherwise was wrong.

Drop the detector (the app already permits cleartext globally for ws:// relays,
so it produced little signal) and correct the misleading comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:33:14 -04:00
Vitor PamplonaandGitHub dfde5336f1 Merge pull request #3570 from vitorpamplona/claude/messages-label-styling-dbp4ov
feat: style Messages room-type labels as Concord-style pills
2026-07-15 09:57:27 -04:00
Vitor PamplonaandGitHub f59b1e5c52 Merge pull request #3567 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-15 09:20:12 -04:00
Vitor PamplonaandGitHub 18e6e83a3a Merge pull request #3552 from nrobi144/feat/desktop-note-scheduling
feat(desktop): note scheduling + NIP-37 draft sync (shared scheduling in commons)
2026-07-15 09:20:03 -04:00
Vitor PamplonaandGitHub 7680dfbce6 Merge pull request #3568 from vitorpamplona/claude/marmot-group-icons-tlcwa1
Add MIP-01 v2 group avatar encryption and upload support
2026-07-15 08:14:25 -04:00
Vitor PamplonaandGitHub 51607ff4cf Merge pull request #3566 from vitorpamplona/claude/concord-quartz-amethyst-plan-0oy779
Concord: end-to-end-encrypted communities (CORD-01…07) on Android + CLI
2026-07-14 23:14:36 -04:00
Vitor PamplonaandGitHub 22c2305e93 Merge pull request #3565 from vitorpamplona/claude/public-chat-creation-fab-p9s72t
Add floating action button to create new public chats
2026-07-14 22:00:44 -04:00
Vitor PamplonaandClaude Opus 4.8 984cafd1fb fix(concord): community title fallback + round channel-create FAB
The channel-list screen titled itself `state.metadata.name ?: app_name`, so
before the metadata edition folded it showed the app's own name — "Amy Debug"
in a debug build. Prefer the folded name, then the stored community name from
the list entry (always present from the join/create, and what shows everywhere
else); the app-name fallback is now effectively unreachable.

The channel-create FAB used Material 3's default rounded-square shape; every
other FAB in the app is a circle. Set shape = CircleShape to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:00:04 -04:00
Vitor PamplonaandClaude Opus 4.8 edbc910941 fix(concord): upload community icon/banner off the main thread
Picking a community icon or banner always failed with "Failed to upload media".
The upload was launched from the form's rememberCoroutineScope() (the Compose
Main dispatcher), so the very first pipeline step — MediaCompressor.compress —
hit Amethyst's checkNotInMainThread() guard and threw OnMainThreadException
before any bytes left the device.

Run ConcordImageUploader.uploadEncrypted inside withContext(Dispatchers.IO) so
the whole compress → strip → AES-GCM-encrypt → Blossom pipeline is off-main; the
Compose state write stays on the launching (Main) scope.

Also stop the form's catch from swallowing the real cause: surface the actual
exception message in the toast (falling back to the generic string only when it
has none), log it, and rethrow CancellationException instead of eating it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:00:04 -04:00
Vitor PamplonaandClaude Opus 4.8 5b54eb88a5 fix(concord): notify on reactions to my Concord messages
NotificationFeedFilter derived `isConcord` from the note's own gatherers
(is it in a joined ConcordChannel), but LocalCache.consumeConcordRumor only
attaches kind-9 messages and kind-1111 replies to the channel — never a kind-7
reaction. So a reaction's `isConcord` was always false, it didn't bypass the
follow filter, and since a fellow member usually isn't a follow it was dropped
in Curated/Selected mode.

Recognize a reaction/repost as Concord through its TARGET instead: if
`replyTo.lastOrNull()` is a message in a community I've joined, it bypasses the
follow filter exactly like a reply. Relevance is still the existing p-tag gate,
so only reactions that actually tag me notify (a well-formed NIP-25 kind-7
p-tags the reacted author, which is what our own ChannelChat.reaction writes).
The "Messages in notifications" toggle now gates only Concord messages, not
reactions — a like isn't a message, so it follows the same rule as any other
reaction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:58:48 -04:00
Vitor PamplonaandClaude Opus 4.8 08c1d1d539 fix(concord): show the quoted parent on kind-9 chat replies in NoteCompose
A Concord (and MLS/WhiteNoise) chat reply is a kind-9 ChatEvent that carries
its reply target as a NIP-18 `q` (or NIP-10 `e`) tag, not a NIP-10 thread — so
it isn't a BaseThreadedEvent and RenderTextEvent's reply-to preview never fires
for it. On the chat feed the preview is drawn by chat-only code, but everywhere
else NoteCompose routes kind-9 through RenderChat, which rendered only the
content and never `note.replyTo`. Result: on the Notifications tab a Concord
reply showed no quoted parent (no border) — most visibly when replying to an
image, whose target is likewise a kind-9.

RenderChat now takes unPackReply and, when FULL and not makeItShort, renders
ReplyNoteComposition(note.replyTo.lastOrNull()) like the threaded path does,
skipping it when the parent is already cited inline (`nostr:...`) so an
MLS-style quote isn't drawn twice. NoteCompose forwards unPackReply; the thread
view passes NONE since its structure already shows the parent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:58:48 -04:00
Vitor PamplonaandClaude Opus 4.8 4ec44ad241 feat(concord): harvest full member roster from bounded channel history
The members roster was a fraction of the real membership (e.g. ~13 vs ~44 on
Armada). Concord membership includes every "observed author" (CORD-02 §5 — anyone
seen publishing), but the live channel subs only carry the recent tail the relay
serves, so most members — who posted outside that tail and never sent a Guestbook
Join — never appeared.

Add ConcordMemberHarvest: a headless, run-once background sweep mounted by the
members screen that pages every folded channel's history back to a bounded window
(90 days — tunable; bounds the data pulled onto the device, per the "how far back"
limit) in one pooled `fetchAllPagesFromPool`. The wraps ride the app's normal ingest
(global CacheClientConnector → concordSessions.ingest), which folds each author into
`observedAuthors`, so the roster fills in with no extra plumbing. AUTH is free — the
channel stream keys are already registered for these relays. `beginMemberHarvest()`
gates it to once per community.

Prerequisite fix: `ConcordCommunitySession.ingest` re-decrypted a channel's WHOLE
wrap buffer on every incoming message (reprojectChannel), which is O(n²) in the
message count — fine for a ~50-wrap live tail but fatal for a history sweep. Split
it: a message now projects only its own wrap (O(1)); the re-decrypt-all path stays
for a re-fold (where channel keys can change). This also speeds the live path.
`ConcordCommunitySessionTest` now asserts the one-wrap-per-message projection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:58:48 -04:00
Vitor PamplonaandGitHub 20479ea0ef Merge pull request #3564 from vitorpamplona/claude/eager-cannon-ykmjry
Fix NoteHeaderFirstRowDensityPreview to use tryEmit for Flow
2026-07-14 19:40:52 -04:00
Vitor PamplonaandGitHub 89bb4063ae Merge pull request #3563 from vitorpamplona/claude/nip29-preload-startup-92ila9
Surface NIP-29 group replies and likes in notifications
2026-07-14 19:28:48 -04:00
Vitor PamplonaandClaude Opus 4.8 d84555a27b fix(concord): real role names + full member roster (CORD-02 §5 / CORD-04)
Two roster gaps against the reference client (Armada):

1. Moderators showed as "Admin" and role definitions were often empty. The
   displayed roles came from ConcordCommunityState.roles, which was built from the
   RAW structural fold heads — so a rogue higher-version edition on a role's
   coordinate (e.g. marking the Admin role deleted) corrupted or emptied the roster,
   and even when present the UI collapsed every role-holder to a single "Admin"
   badge. Now state.roles comes from the authority-gated resolver
   (AuthorityResolver.roles(), exposed alongside rolesFor()), and ConcordMembersScreen
   renders each member's actual most-privileged role name (Admin / Moderator / custom).

2. Member count was a fraction of the real one (e.g. 10 vs ~44). CORD-02 §5: "an
   author seen publishing is observably present, auto-included even if their Join
   never arrived." The roster only counted Guestbook joiners + the privileged roster,
   omitting the bulk of members who never post a Join. ConcordCommunitySession now
   tracks observedAuthors from every decrypted channel message and folds them into
   allMembers() and the roster.

Also: amy's `concord roles/grant/ban/...` now register the control-plane stream key
before draining (like `channels`/`read`/`send` already do), so the mod verbs aren't
served an empty fold on NIP-42-gated relays — used to ground-truth the resolved roles.

Verified via amy against live Soapbox: `concord roles` now returns Admin (pos 1) and
Moderator (pos 2) instead of []. quartz + commons concord suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:13:47 -04:00
Vitor PamplonaandGitHub dfc3c08f0b Merge pull request #3562 from vitorpamplona/claude/amy-graperank-commands-review-mofb8g
Refactor GrapeRank into separable pipeline stages with local persistence
2026-07-14 18:53:24 -04:00
Vitor PamplonaandClaude Opus 4.8 83c985819f fix(concord): render the decrypted community icon (local file:// avatar)
A Concord community icon is CORD-02 §6 encrypted media: rememberConcordImageModel
fetches the ciphertext, AES-256-GCM-decrypts it, and caches the plaintext at a
local file:// path, which it hands to the avatar as the model. But
RobohashFallbackAsyncImage always wrapped the model in ProfilePictureUrl, and the
thumbnail-cache fetcher behind it (ProfilePictureFetcher) delegates a cache-miss
to Coil's http-only NetworkFetcher — so the file:// load failed and the row fell
back to the robohash, even though the icon had decrypted and cached correctly.

Only route remote http(s) pictures through the thumbnail cache; hand local/content
URIs straight to Coil's native fetchers, which load them directly. Verified
on-device: the Soapbox community icon now renders on the Messages rows and the
community header instead of a robohash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:10:21 -04:00
Vitor PamplonaandClaude Opus 4.8 92f422be88 fix(concord): authority-gate the control-plane fold (CORD-04 §1)
The fold selected each entity's head by STRUCTURAL fold only and never dropped
editions from unauthorized signers, so a spoofed edition that structurally
supersedes a legit one won — on the live Soapbox community this surfaced a decoy
metadata edition ("invalid ... clients should be ignoring this", no icon) over
the real owner-authorized one, so no community icon/banner ever rendered.

Two stacked wire/authority bugs, both fixed:

- RoleEntity.scope was typed String, but the reference client (Armada) writes it
  as an object ({"kind":"server"} / {"kind":"channel","channel_id":...}) per
  CORD-04 §2. The type mismatch failed the whole RoleEntity decode, dropping the
  role — and with it every grant depending on it — so no admin ever resolved.
  Added RoleScope and retyped the field (no writer set it, so no migration).

- AuthorityResolver.resolve now folds each role/grant/banlist CHAIN through
  authorized editions only, via an owner-rooted fixpoint over the full edition
  set (not the post-hoc structural heads). A rogue higher-version grant from an
  unprivileged key is dropped instead of superseding the owner's grant, so the
  legit authority stands. ConcordCommunityState.fold then gates metadata by
  MANAGE_METADATA, channels by MANAGE_CHANNELS, the banlist by BAN, and the
  dissolution tombstone to the owner alone.

Verified live end-to-end: `amy concord channels Soapbox` now folds the real
metadata (name "Soapbox Community", the blossom icon + banner pointers) and all
9 channels; fetching + AES-256-GCM-decrypting the icon pointer yields a
hash-verified PNG. Regression tests cover the object-scoped role and the rogue
superseding grant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandClaude Opus 4.8 122403baf4 feat(cli): AUTH as the Concord plane stream key so amy can read planes
Concord relays gate a plane's kind-1059 wraps behind NIP-42 and serve them
only to a connection authenticated AS the plane's derived stream key — the
member is neither the wrap's author (the stream key) nor its recipient (a
throwaway ephemeral key), so an account AUTH is refused and every plane REQ
came back empty (no channels, no messages), even though the community folded
its name.

Mirror the app's per-plane AUTH (60475c10c0) in the CLI:
- Context.registerConcordStreamKeys(relays, secrets) records the derived
  control/channel stream secrets, scoped to the community's relays.
- The RelayAuthenticator provider now signs one kind-22242 per registered
  stream key alongside the account AUTH — locally, from the raw derived key
  (NostrSignerSync), never the account, so no user identity is exposed.
- The concord channels/read/send verbs register their control + channel
  stream keys before draining/publishing.
- drain() gains pendingOnAuthRequired: an auth-required CLOSED keeps the relay
  pending instead of terminal, so the post-auth subscription re-fire delivers
  the events rather than the one-shot drain returning empty. The concord verbs
  opt in; all other drains are unchanged.

Verified end-to-end against the live Soapbox community (relay.ditto.pub /
relay.dreamith.to): `amy concord channels` now folds the name + 9 channels
consistently and `amy concord read` returns messages. `channels` also emits
the folded icon/banner/description pointers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandClaude Opus 4.8 a4d9087187 fix(concord): include the 32-zero id in the invite bundle key (CORD-05)
CORD-05 Appendix A.1 says the HKDF `id` is always present — 32 bytes,
all-zero for a label with no meaningful id — and A.6 lists
`concord/invite-key` with `id = 0…0`. inviteBundleKey built its info with
no id at all, so it derived a different bundle key than the reference
client (Armada) and NIP-44 decryption of an Armada-minted invite bundle
failed with "Invalid Mac" — the join aborted with "no valid bundle for
this link". amy round-tripped with itself (same wrong key both sides), so
its own tests passed and the divergence went unnoticed.

Verified by decrypting a real Armada invite bundle: the no-id key fails the
MAC; the zero-id key yields a valid CommunityInvite JSON. Pass ByteArray(32)
like the sibling banlist/dissolved derivations already do.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandClaude Opus 4.8 8060dc0bb1 fix(concord): fold refounded community control plane for fresh joiners
After a Refounding (CORD-06 §3), compaction re-wraps each entity's head
verbatim, so a head edited past genesis still carries an `ep`/prev citing
an edition in the prior epoch that a fresh joiner never fetches.
EditionFold.foldEntity required a genesis edition (prevHash == null) and
returned null otherwise, so a fresh login folded the whole Control Plane to
nothing: no community icon, no name, no edited channels (while Armada,
which implements the CORD-04 §1 fresh-joiner rule, showed them all).

Anchor at the lowest-version edition when no genesis is present and walk up
from there. Safe: Amethyst always re-folds the whole buffer from scratch
(no persistent floor), so it is structurally always a fresh joiner;
authority is validated on top by AuthorityResolver, so an unrooted forgery
is still dropped; and a genuine mid-chain gap still stops at the intact
prefix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandGitHub 9d5bd48164 Merge pull request #3561 from vitorpamplona/claude/nip-29-channel-icons-8h1dop
Warn users when relay group relay doesn't advertise NIP-29
2026-07-14 16:16:15 -04:00
Vitor PamplonaandGitHub 96c1a2ee2d Merge pull request #3555 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-14 11:43:37 -04:00
Vitor PamplonaandGitHub 8fc841e50e Merge pull request #3559 from vitorpamplona/claude/notecompose-pow-pill-style-vr6hbt
Unify note-header markers into a pill / quiet-mark design system
2026-07-14 11:19:34 -04:00
Vitor PamplonaandGitHub 596cb24de4 Merge pull request #3553 from vitorpamplona/claude/large-screen-layout-v5otrb
Adaptive large-screen layout: nav rail, permanent drawer, notification panel, reading-column cap
2026-07-14 11:19:10 -04:00
Vitor PamplonaandGitHub 01c4ee1870 Merge pull request #3558 from vitorpamplona/claude/community-approval-parse-error-vneyon
Improve NIP-72 approval event parsing robustness
2026-07-14 10:56:19 -04:00
Vitor PamplonaandGitHub ee49c8a987 Merge pull request #3557 from vitorpamplona/claude/antispam-filter-null-pointer-hkx70i
Fix null pointer in AntiSpamFilter when LRU cache evicts duplicates
2026-07-14 10:55:29 -04:00
Vitor PamplonaandGitHub a915a03c32 Merge pull request #3556 from vitorpamplona/claude/left-drawer-banner-crop-bkxw9f
Fix profile banner image scaling from FillWidth to Crop
2026-07-14 10:54:18 -04:00
Vitor PamplonaandGitHub d1fb2ed42c Merge pull request #3551 from vitorpamplona/claude/mobile-battery-optimization-s7vw4a
Battery: background-usage fixes, 122-relay ping study, and an on-device resource-usage ledger
2026-07-14 10:48:54 -04:00
Vitor PamplonaandGitHub 4fcae15bcb Merge pull request #3554 from vitorpamplona/claude/flatpak-desktop-release-ci-wfbcre
Add Flatpak bundle support to release CI + Flathub submission
2026-07-14 10:48:19 -04:00
Vitor Pamplona efae1dec22 Adjustments 2026-07-14 08:21:44 -04:00
Vitor PamplonaandGitHub d043f4d143 Merge pull request #3549 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-13 20:42:21 -04:00
Vitor PamplonaandGitHub 7c82c9cc17 Merge pull request #3550 from vitorpamplona/claude/1059-empty-content-filter-nge9qp
Decouple Android notification kinds from Desktop subscription list
2026-07-13 20:36:57 -04:00
Vitor PamplonaandGitHub 107777369f Merge pull request #3548 from vitorpamplona/claude/user-nicknames-contactcard-mglbz8
Nickname users via NIP-85 contact cards (encrypted petname + private note, custom emojis)
2026-07-13 20:16:09 -04:00
Vitor PamplonaandClaude Opus 4.8 8e30349cbd fix(concord): open a minichat reply in its thread, and resolve the plane from the reply
Tapping a minichat reply (a kind-1111 CommentEvent) in notifications routed to the whole channel:
routeFor(note) matched the reply's Concord / relay-group / public-chat gatherer and opened the
channel, swallowing the thread it was replying in. routeFor now recognizes a chat-context CommentEvent
as a minichat reply and routes to Route.ChatMinichat(rootId) — rootId being the reply's NIP-22 root
(the parent message), for all three chat contexts.

For Concord, the parent message may not be cached (a cold reply notification), and MinichatScreen used
to resolve the plane only from the root note's gatherer — so an unloaded parent could never pick a
relay. The reply itself arrived over the channel plane, so its ConcordChannel gatherer carries the
community/channel: Route.ChatMinichat now also threads concordCommunityId/concordChannelId from the
reply, and MinichatScreen uses them to mount the plane subscription + this channel's backward-history
pager, paging until the parent message loads. Its whole kind-1111 thread then projects normally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 18:53:58 -04:00
Vitor Pamplona 07ad868de1 Merge remote-tracking branch 'upstream/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-13 18:30:21 -04:00
Vitor Pamplona 77300578e9 Merge remote-tracking branch 'upstream/claude/concord-quartz-amethyst-plan-0oy779' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-13 18:29:08 -04:00
Vitor PamplonaandClaude Opus 4.8 5de55741a2 feat(concord): reuse the standard compressed/encrypted upload pipeline for community images
ConcordImageUploader now drives UploadOrchestrator.uploadEncrypted — the same
path DM/chat encrypted media uses — instead of a hand-rolled BlossomUploader
call. A community icon now gets image compression, EXIF/metadata stripping, and
the account's configured Blossom server, keeping the simple photo picker. It
hands the orchestrator a fresh AESGCM cipher and maps the result — ciphertext
url + plaintext hashBeforeEncryption — into the CORD-02 §6 ImagePointer, which
the read path (rememberConcordImageModel) round-trips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 18:22:33 -04:00
Vitor PamplonaandClaude Opus 4.8 66a5e111d6 fix(concord): eagerly backfill an open channel's history to a target window
The live plane subscription only carries each channel's relay-capped recent
tail, shared across one merged REQ per relay for every channel, so a channel
with plenty of history opened showing just its last few messages until the user
scrolled. The old bootstrap only paged when the feed was completely empty.

ConcordBackfillHistoryToWindow now pages older history on open until the feed
holds at least CONCORD_HISTORY_TARGET (50) messages or the relays are exhausted
— mirroring Armada's multi-page backfillStore — page by page via the existing
BackwardRelayPager, then latches off and lets scrolling drive further paging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 18:22:32 -04:00
Vitor PamplonaandClaude Opus 4.8 9f55794e06 fix(concord): only bump session revision on structural change, not per message
Every inbound plane wrap that a session claimed — including a plain chat
message — bumped ConcordSessionManager.revision, and the always-on preload,
the channel subscription, and the open-channel history subscription each call
invalidateFilters() on every bump. So every message re-derived and re-REQ'd
every community's control + channel planes. On a cold load of hundreds of
buffered messages that is hundreds of re-subscriptions, which the relays answer
with "there is a bug in the client, no one should be making so many requests"
and close the plane subs mid-load (each needing a fresh NIP-42 AUTH). The
result: channels load only their last few messages, or none.

ingest() now reports a ConcordIngestOutcome (NOT_MINE / NON_STRUCTURAL /
STRUCTURAL). Only a STRUCTURAL wrap — a Control-Plane fold, a guestbook
membership change, or a buffered base-rekey — bumps the revision. Chat messages
are NON_STRUCTURAL: they still reach the feed via the rumor sink → LocalCache,
but no longer churn the subscriptions. The manager keeps its Boolean contract
(claimed) for DecryptAndIndexProcessor. Verified on-device: the "so many
requests" rate-limit is gone and the plane subscription stays open and drains
steadily instead of being closed and reopened per message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 18:22:17 -04:00
Vitor PamplonaandClaude Opus 4.8 413bf726fd feat(concord): author encrypted community icons (encrypt + Blossom upload)
Completes the CORD-02 §6 image write path: the community-metadata form's
icon hero is now a photo picker that AES-256-GCM-encrypts the chosen image
under a fresh key/nonce, uploads the *ciphertext* as an opaque blob to the
account's Blossom server, and seals the resulting ImagePointer
{url,key,nonce,hash} into the metadata — the inverse of the read path, and
what Armada does in concord-v2 (`encryptImageBlob` + Blossom upload).

- ConcordImageUploader reuses the existing NIP-17 DM encrypted-media
  primitives (AESGCM + BlossomUploader.upload(inputStream, …) + the account's
  Blossom server list + createBlossomUploadAuth). The blob is content-
  addressed by the ciphertext SHA-256; the pointer's hash is the plaintext
  SHA-256 for read-side integrity.
- ConcordMetadataFields now holds an ImagePointer? and its hero opens the
  photo picker (spinner while uploading), replacing the plain-URL field — a
  URL-string icon was never CORD-02-valid (Armada renders robohash for it).
- Create/edit pass the encrypted pointer straight through to
  createConcordCommunity / editConcordMetadata.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:32:56 -04:00
Vitor PamplonaandClaude Opus 4.8 28b93132a3 feat(concord): read + render CORD-02 §6 encrypted community icon/banner
Concord community icons never showed (robohash instead), and the community
name silently fell back to the invite name. Root cause: the icon/banner are
CORD-02 §6 **encrypted media** — the metadata entity carries an
`ImagePointer` object `{url,key,nonce,hash}` (AES-256-GCM ciphertext at
`url`, decrypted with `key`/`nonce`, `hash` = SHA-256 of the plaintext) —
but `MetadataEntity.icon` was typed `String?`. An object where a String is
expected fails the whole entity's decode, so metadata came back null: no
icon, and the name dropped to the entry fallback. Matches the Concord v2
reference client (Armada `concord-v2/lib/{types,image}.ts`).

- Promote `ImagePointer` to a shared CORD-02 type (was invite-only) and give
  it `decryptOrNull` (AES-256-GCM via the existing `AESGCM`, verifying the
  plaintext SHA-256 — a swapped blob fails closed).
- `MetadataEntity.icon`/`banner` are now `ImagePointer?`, so the entity (and
  the community name) decodes. `ConcordChannel` carries the pointers.
- `rememberConcordImageModel` resolves a pointer for the avatar: a plain-URL
  pointer (Amethyst's own form) passes through; an encrypted one is fetched,
  decrypted, verified, cached to disk, and rendered — else the robohash. Wired
  into the Concord hub avatars and the Messages-tab community chip.
- Amethyst's create/edit still take a URL and wrap it as a url-only pointer;
  authoring encrypted images (encrypt + upload) is a follow-up.

Adds ImagePointerTest: Armada-shape object decode, decrypt round-trip, and
fail-closed on a tampered hash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:16:12 -04:00
Vitor PamplonaandGitHub beaae90eb5 Merge pull request #3547 from vitorpamplona/claude/build-oom-investigation-ryd9u6
fix(ci): serialize :amethyst Kotlin compilations on CI to stop daemon OOMs
2026-07-13 16:50:39 -04:00
Vitor PamplonaandClaude Opus 4.8 975ccb9cf3 feat(concord): preload community control planes app-wide, like DMs
Communities only folded — revealing their channels, metadata (name/icon)
and membership — while a Concord screen was actually open, because the
Control-Plane subscription (ConcordChannelSubscription) was mounted only on
the six Concord screens. Sit on the Home feed and nothing streams in; a
community you haven't opened stays unfolded (no channels, no image).

Concord control-plane wraps are addressed to derived stream keys, not
`#p=self`, so the always-on account/DM gift-wrap tail can't pick them up.

Add ConcordChannelPreload — an account-level, always-on mount using the
non-lifecycle KeyDataSourceSubscription (the same primitive
AccountFilterAssemblerSubscription uses for DMs) with the existing
revision-driven filter re-derivation — and mount it in LoggedInPage next to
the DM/account preload. Now every joined community's planes are requested
from login regardless of screen, so folds happen in the background. The
already-always-on ingest/refreshConcordChannelIndex path was only missing
this continuous network request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:40:45 -04:00
Vitor PamplonaandClaude Opus 4.8 95ab9e6528 fix(desktop,cli): match the new interactive auth-callback signature
The relay-auth fix added an `interactive` flag to
RelayAuthenticator.signWithAllLoggedInUsers; update the desktop and cli
implementers (which don't prompt) to the 3-arg lambda.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:14:30 -04:00
Vitor Pamplona 04388adc02 Merge remote-tracking branch 'upstream/claude/concord-quartz-amethyst-plan-0oy779' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-13 14:07:42 -04:00
Vitor PamplonaandClaude Opus 4.8 2b4148ef19 feat(concord): scroll-back history pagination for channels
Concord channels only showed the recent tail the relay served for the
channel plane and never loaded older messages on scroll. Add backward
`until`+`limit` paging, per relay, on demand — the same model as the
NIP-04 per-conversation history.

Reuses the shared paging stack as-is (BackwardRelayPager,
RelayLoadingCursors, the RelayReach* markers/sentinels and
DmHistoryLoadingCard); only the Concord-specific data layer is new:

- ConcordChannel holds a per-channel RelayLoadingCursors (`history`), so
  cursors share the channel's cache lifetime.
- ConcordChannelHistory{FilterAssembler,SubAssembler} binds a pager to the
  open channel, builds `{kinds:[1059], authors:[planePk], until, limit}`
  per armed relay, and forwards relay callbacks; registered in
  RelaySubscriptionsCoordinator and mounted by the channel screen.
- ConcordCommunitySession.channelPlaneAddress() resolves a channel's REQ
  author from the fold.
- ConcordChannelScreen wires the olderBoundary/markersInGap/sentinels feed
  hooks and bootstraps an empty channel.

The history floor is `now` (not the DM 7-day tail): the Concord live sub
isn't a strict recent-tail (it asks the plane author unbounded and the
relay caps the result), so paging must walk the whole history from the top
to reach recent-but-capped messages. Overlap with the live tail is
harmless — wraps dedup by id on ingest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:04:21 -04:00
Vitor PamplonaandClaude Opus 4.8 691adb361c fix(concord): drop deleted-message ghost rows in channels
A Concord chat row rendered a permanent "Event is loading or can't be
found in your relay list" placeholder when a message's kind-5 delete was
processed before the message itself — easy to hit because a reproject
re-emits the whole wrap buffer and wrap ordering isn't guaranteed.

`consumeConcordRumor` attaches the row before `justConsume`, but
`justConsume` bails without loading the event once the rumor has been
deleted, leaving an event-null note pinned in the channel forever.

Skip attaching a row for a rumor already known deleted (also avoids
add/remove churn on every reproject), and after consuming, drop the row if
its event never loaded. The reverse order (delete after the message) is
still handled by the normal deletion cascade unlinking the note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:03:55 -04:00
Vitor PamplonaandClaude Opus 4.8 0e485349e3 fix(relay-auth): re-authenticate on an auth-required CLOSED (NIP-42)
Concord channels loaded empty because the channel-plane REQ was refused
until the relay happened to re-issue an AUTH challenge (~27s later, or
never on relays that don't re-challenge).

A channel's derived stream key only exists after the community's Control
Plane folds, but by then the connection's initial NIP-42 AUTH already ran
with just the control key. `RelayAuthenticator` only re-authenticated on a
fresh `AUTH` message and ignored `auth-required` CLOSED frames — so the
newly-revealed channel keys were never sent.

NIP-42 says the client must store the connection's challenge and reuse it
"in response to the auth-required CLOSED message". Do that: remember the
last challenge per relay, and on an `auth-required:` CLOSED re-run the
sign/send pass with it. `saveAuthSubmission` dedups by (pubkey, challenge)
so only not-yet-authed identities (the folded-in keys) are sent — a no-op
once they all are, so no loop. A burst guard skips re-signing while an AUTH
is already in flight (syncFilters re-drives the REQ when it settles), and
the re-auth is non-interactive: it re-sends only already-approved
identities (ledger-ALLOW accounts + stream keys) and never raises a prompt,
so it can't drag a bystander account onto a paid relay.

Adds an `interactive` flag to the signing callback and a
RelayAuthenticatorReauthOnClosedTest covering reuse, loop-safety,
burst-coalescing, and the non-auth-required CLOSED no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:03:37 -04:00
Vitor Pamplona 43d0b8b2bc Merge remote-tracking branch 'upstream/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-13 12:43:12 -04:00
Vitor PamplonaandGitHub b6560ab3f4 Merge pull request #3546 from vitorpamplona/claude/compose-chip-notification-toggle-6nmy6h
Replace remove-from-notify with mute/unmute bell toggle
2026-07-13 11:23:01 -04:00
Vitor PamplonaandGitHub 9d79d3bc7f Merge pull request #3545 from vitorpamplona/claude/amethyst-video-repeat-limit-bq2ut7
Add AutoReplayLimiter to cap video auto-loops in feed
2026-07-13 08:11:28 -04:00
Vitor PamplonaandGitHub a2e858d7b4 Merge pull request #3544 from vitorpamplona/claude/pr-note-button-sizing-732r64
Clean up Git card styling and fix duplicated subject in content
2026-07-12 21:28:11 -04:00
Vitor PamplonaandGitHub 82fa652c19 Merge pull request #3543 from vitorpamplona/fix/inbox-nostr-wine-over-auth
fix(relay-auth): stop paid relays billing accounts that don't use them (inbox.nostr.wine over-AUTH)
2026-07-12 21:05:35 -04:00
Vitor PamplonaandClaude Opus 4.8 6a528a742c fix(relay-auth): show a relay's payment NOTIFY only under the billed account
The relay NOTIFY (payment prompt) went into a process-wide NotifyRequestsCache
and DisplayNotifyMessages showed it under any account whose relay list contained
the relay — so a prompt billed to Amethyst surfaced under Vitor, and stale
entries re-appeared on every account switch.

A NOTIFY doesn't reliably name a pubkey, so instead of parsing the message we
correlate it with the AUTH that triggered it: a paid relay answers an
unauthorized AUTH with `OK <authEventId> false …` right before the NOTIFY, and we
signed that auth event. NotifyCoordinator remembers each auth event's signer,
maps the failing OK back to it, and files the NOTIFY into THAT account's own
Account.relayNotifications. Unattributable NOTIFYs are dropped. DisplayNotifyMessages
now reads only the current account's cache.

Also removes the now-unused app-wide RelayAuthPermissionStore singleton and wires
NotifyCoordinator with the pubkey -> Account lookup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:32:24 -04:00
Vitor PamplonaandClaude Opus 4.8 0ec63958b8 fix(relay-auth): only AUTH a relay with accounts that actually use it
One shared NostrClient serves every logged-in account, so a relay's NIP-42 AUTH
challenge is not tied to any one of them. AuthCoordinator used to fold every
account's verdict into one decision and then sign with EVERY account (or a random
throwaway key), so a paid relay like inbox.nostr.wine billed and de-anonymized
accounts that never used it — including via a merged read filter that merely named
them, and via account switches / the background notification consumer.

Decide and sign PER ACCOUNT now: an account signs only if the relay is in its own
relay list, or it is publishing its own event there (RelayAuthFirstParty), AND its
own ledger verdict (Account.relayAuthLedger) allows it. A subscription merely
naming the account is NOT first-party — that is the merged-filter false positive.
The random-ephemeral-key fallback is removed. Own-inbox reads still qualify: the
relay serving them is by definition in the account's own list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:32:24 -04:00
Vitor PamplonaandClaude Opus 4.8 f8416f4940 feat(relay-auth): per-account NIP-42 override store + state on Account
Per-relay ALLOW/DENY AUTH overrides (and the policy ledger that reads them)
lived in one process-wide DataStore keyed only by relay URL, so a DENY set for
one account silently applied to every logged-in account. Move them to a
per-account file under accounts/<pubkey>/ and warm-cache the overrides in memory
on the Account (RelayAuthPermissionCache) so an AUTH challenge is answered
without a disk read.

DataStoreRelayAuthPermissionStore now shares one DataStore per file path —
DataStore v1 forbids two live instances on one file, and loadAccount can build
the store more than once per account.

Introduces the per-account state holders wired up by the following commits:
Account.relayAuthPermissions, relayAuthLedger and relayNotifications.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:32:24 -04:00
Vitor PamplonaandGitHub 00e02048ea Merge pull request #3536 from davotoula/feat/kover-coverage
Aggregate Kover coverage (for SonarQube import)
2026-07-11 11:02:23 -04:00
Vitor PamplonaandGitHub aff10df508 Merge pull request #3535 from vitorpamplona/claude/nostr-metadata-parse-errors-jmzlyl
fix: don't drop whole kind-0 profiles over one mistyped field
2026-07-11 10:38:07 -04:00
Vitor PamplonaandGitHub a5f9a234c1 Merge pull request #3534 from vitorpamplona/claude/auth-permission-message-text-moco75
Clarify relay auth descriptions for profile and engagement data
2026-07-11 10:28:13 -04:00
Vitor PamplonaandGitHub 8e38ea6607 Merge pull request #3533 from vitorpamplona/claude/amethyst-miner-performance-0rw1dn
Parallelize PoW mining with multi-worker nonce search
2026-07-11 09:54:01 -04:00
Vitor PamplonaandGitHub b64c378a9d Merge pull request #3519 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-10 23:20:26 -04:00
Vitor PamplonaandGitHub 5c8801082a Merge pull request #3513 from vitorpamplona/claude/amy-graperank-crawl-perf-jhmx0x
GrapeRank crawl: connect once, wait once — 38% faster at hop-3; hop-8 31% faster at +15% lists / +47% users
2026-07-10 23:19:58 -04:00
Vitor PamplonaandGitHub 4cc88cc0ae Merge pull request #3529 from vitorpamplona/claude/powdisplay-notecompose-sizing-x8ua7i
Reduce PoW badge size for better UI balance
2026-07-10 23:19:34 -04:00
Vitor PamplonaandGitHub d6c046249e Merge pull request #3528 from vitorpamplona/claude/fix-signed-events-reference-vhrzsa
Mark DM rooms as read when wraps are mined in background
2026-07-10 23:14:35 -04:00
Vitor PamplonaandGitHub c889952e58 Merge pull request #3526 from vitorpamplona/claude/relay-nip29-groups-button-gadubu
Add NIP-29 relay groups button to relay information screen
2026-07-10 21:48:20 -04:00
Vitor PamplonaandGitHub e60003063b Merge pull request #3527 from vitorpamplona/claude/nip13-pow-publishing-queue-1takp1
Add NIP-13 Proof-of-Work mining queue with persistence and UI
2026-07-10 21:48:03 -04:00
Vitor PamplonaandGitHub 0eb2beda22 Merge pull request #3525 from vitorpamplona/claude/dm-unread-own-messages-jl18pt
Fix DM read state: mark rooms as read when sending messages
2026-07-10 20:55:14 -04:00
Vitor PamplonaandGitHub 57aaf69073 Merge pull request #3524 from vitorpamplona/claude/relay-icons-display-timing-7c3n7c
Fix relay attribution in gift-wrap chains and Marmot messages
2026-07-10 19:11:47 -04:00
Vitor PamplonaandGitHub eaf29a265d Merge pull request #3523 from vitorpamplona/claude/markdown-hashtag-fragments-9ya187
Support markdown hashtag links (#tag fragments)
2026-07-10 19:04:02 -04:00
Vitor PamplonaandGitHub 84d16348df Merge pull request #3520 from vitorpamplona/claude/auth-permission-architecture-weyn9x
NIP-42 relay auth: contextual "why", per-situation trust, and prompts
2026-07-10 18:48:54 -04:00
Vitor PamplonaandGitHub 4d16d6e38f Merge pull request #3522 from vitorpamplona/claude/richtext-trailing-whitespace-85eudl
Strip trailing whitespace from rich text parser output
2026-07-10 18:11:21 -04:00
Vitor PamplonaandGitHub 581fded5f5 Merge pull request #3521 from vitorpamplona/claude/brainstorm-world-client-033mhn
Update Brainstorm web app icon URL
2026-07-10 18:06:24 -04:00