Commit Graph
16961 Commits
Author SHA1 Message Date
vitorpamplonaandgithub-actions[bot] 6139923561 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-15 20:10:21 +00: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
Claude 00e5443247 fix(concord): only fill content from imeta URLs when content is blank
Narrow appendMissingImetaUrls to fire only when the message content is
blank: in that case return the imeta URLs joined one per line, otherwise
leave the content exactly as-is. A message that already carries text is
never rewritten, so inline URLs and ordering are preserved untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SF85F6GHu7aQZYpTGNYdWk
2026-07-15 19:53:36 +00:00
Claude 8c9c6e9e90 fix(nip29): preload recent group chat, not just the roster
The joined-groups preload (RelayGroupMyJoinedGroupsFilterAssembler) only
subscribed to roster kinds (39000/39001/39002). It never requested the
kind-9 chat timeline, so the Messages-list preview for a group reflected
only whatever chat events happened to already be in LocalCache from
unrelated subscriptions — reading as "scattered" messages — and the full,
recent timeline was fetched for the first time only when the group was
opened (filterMessagesToRelayGroup, limit 200).

Also prefetch a bounded slice of each joined group's most recent chat
(kind 9 + polls, limit 50, `#h`-scoped, pinned to the host relay) alongside
the roster, so list previews show the true newest message and opening a
group lands on already-cached content. Shares the per-relay EOSE/since with
the roster filter, matching the existing warmup/channel assembler pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Vvc7LeGNTpg1ggm5P8AtH
2026-07-15 19:32:12 +00:00
Claude 3892ade0a3 fix(concord): render imeta-only images in the chat feed too
The earlier imeta-only fix patched RenderChat (the thread-view path), but
the Concord channel feed renders messages through ChatMessageCompose →
RenderRegularTextNote, which handed the raw decrypted content straight to
the media renderer. So a NIP-C7/Concord image message from an interop
client (e.g. Ditto/Soapbox Armada) — empty content, image carried only as
a NIP-92 `imeta` tag — never rendered in the feed, even though the
encrypted blob's key/nonce were already registered for transparent
decryption at ingest.

Extract appendMissingImetaUrls into a shared helper (ui/note/types/
ImetaContent.kt) and apply it in RenderRegularTextNote against the
decrypted content, symmetric to the RenderChat fix and to
ChannelChat.imageMessage which appends the URL on send. Normal messages
whose imeta URLs are already inline are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SF85F6GHu7aQZYpTGNYdWk
2026-07-15 19:26:13 +00: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
Claude 029f012fe8 feat(nav): pinned group tabs behave as bottom-nav roots
The public-chat, relay-group and Concord-community screens are reached both as
pushed details (from a list) and as pinned bottom-nav tabs. Give them the same
dual-mode chrome the other tab roots use, keyed off nav.canPop():

- Show the back arrow only when there is something to pop. Added an optional
  showBackButton (defaulting to the current behavior) to TopBarExtensibleWithBackButton
  and passed nav.canPop() from PublicChatTopBar and RelayGroupTopBar; gated the
  Concord list screen's back icon the same way.
- Render AppBottomBar in all three screens. It hides itself when canPop, so a
  pushed instance shows the back arrow and no bar, and a bottom-nav instance
  shows the bar and no back arrow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mxdSFQcarsKaL179tSub1
2026-07-15 18:40:16 +00:00
Claude 7dc033ec79 fix(ui): show NIP-29 group names in the picker, not their ids
The read-only settings picker never fetches a group's kind-39000 metadata, so with
no cached event RelayGroupChannel.toBestDisplayName() fell back to the raw group id.
Resolve the label from the group's metadata name when loaded, otherwise the name the
user's joined-groups list already stored for it (the NIP-51 ["group", id, relay, name]
tag), and only then the id. Also helps the live bar before 39000 arrives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mxdSFQcarsKaL179tSub1
2026-07-15 18:33:19 +00:00
Claude e1a9a5cd7e fix(ui): keep drag alive across swaps + equalize Add/Added pill height
- Drag reorder: key each preview tab by its stable identity. Without a key the
  tabs were position-identified, so when a swap reordered the list, the slot under
  the finger recomposed with a different entry — restarting its pointerInput (keyed
  on the entry) and cancelling the in-flight gesture, so dragging stopped on the
  first swap. Keying moves the dragged composable (and its live gesture) instead.
- Add/Added button: both states now share one Surface + Row body (only color,
  border and tint differ) instead of an OutlinedButton vs a Surface. The
  OutlinedButton's ~40dp min height made "Add" taller than "Added" and broke row
  alignment; a single body keeps the pill height constant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mxdSFQcarsKaL179tSub1
2026-07-15 18:12:19 +00:00
Claude 8212e52a50 fix(ui): expand inner options straight down, not diagonally
AnimatedVisibility's default enter (fadeIn + expandIn from the bottom-end) made
the category / chat child lists slide in from the top-left. Switch both to a pure
vertical expandVertically(Top) / shrinkVertically(Top) so the options unroll
straight down with the toggle, matching the accordion's open gesture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mxdSFQcarsKaL179tSub1
2026-07-15 16:36:09 +00:00
Claude 9180061655 feat(ui): drag-reorder inside the preview bar + real favicons
Address two gaps in the redesigned bottom-bar setup screen.

- The preview IS the editor now: the mini nav bar is the reorder & remove
  surface. Drag a tab within the bar to reorder it (holder moveTransient/commit),
  tap its ✕ badge to remove. Drops the separate chip strip — one WYSIWYG bar
  instead of a preview plus a duplicate list.
- Favorites render their real favicon / nsite / napplet icon (via the same
  FavoriteAppIcon + rememberFavoriteIconModel the live bar uses) instead of a
  generic globe glyph — in the preview tabs and the Browser picker rows alike.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mxdSFQcarsKaL179tSub1
2026-07-15 16:09:01 +00: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
Claude f592cbc2ed fix: refine new-conversation chooser copy and audit fixes
- Private Message trade-off now reads "Can't add users later" (accurate
  for NIP-17: you can't add members to an existing DM room).
- Darken the Concord/Public/Disappearing accents so the white icon-tile
  glyph and the white Create-button label meet WCAG AA contrast (the
  amber/teal/orange were ~2.6–3.4:1 on white text).
- Bound the row title/tagline/chip with maxLines + ellipsis so a longer
  localized string can't wrap and break the one-line row layout.
- Mark ConversationType/ConversationSection @Immutable so ConversationRow
  is skippable — an accordion toggle now recomposes only the two affected
  rows instead of all six.
- Fix ProConRow icon modifier order (padding before size) so the check/
  close glyph is nudged, not squished.
2026-07-15 15:47:53 +00:00
Claude 9892ba495b feat(ui): redesign the bottom-bar setup screen — live preview + tab chips
Give the settings screen a point of view instead of a stock Material list.
Presentation only — same BottomBarEntry model, holder and resolver underneath.

- Live preview: a real mini nav bar at the top renders the pinned tabs (first
  one highlighted like the bar on open) and updates as you add, remove and
  reorder — the screen is now WYSIWYG.
- Your tabs: the pinned set is a horizontal, drag-to-reorder chip strip that
  mirrors the bar's own shape, each chip with a leading avatar/icon and an ✕,
  plus an "n / 5" slot hint that turns red past the recommended count.
- Available catalogue: collapsible category cards with a leading icon tile;
  every option row has a tinted circular leading and an "Add → ✓ Added" pill
  instead of a bare switch, so the control states its action and result.
- Group rows show the real group avatar; Browser / Public Chats / Relay Groups
  / Concord expand to your favorites / joined groups (read-only, no REQ storm).

Reorder reuses the holder's moveTransient/commit; all edit logic stays in the
unit-tested BottomBarSettingsState.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mxdSFQcarsKaL179tSub1
2026-07-15 15:33:29 +00:00
Claude 5e7fffe08e feat: lean, color-coded redesign of the new-conversation chooser
Reworks the "Start a conversation" chooser to be scannable instead of a
wall of text. Each conversation type is now a one-line row — a
color-coded icon tile, the name, a short tagline, and a single chip
naming its deciding axis (1:1–5 rooms / Device-bound / Workspaces /
Unmoderated / Moderated / Live now). Tapping a row expands it in place to
reveal "Best for" and a compact Good / Trade-offs split, then a Create
button that routes to that type's existing flow (accordion: one open at a
time).

Copy now leads with each protocol's real differentiator — Marmot's
device-bound gotcha (chats don't follow an nsec to another app), Concord
as channel-split workspaces, and the relay-aware section grouping Public
Chat + Relay Group by the fact the relay can see and moderate who's there.

Per-type accent colors are lightened in dark theme for legibility; the
solid icon tile keeps the saturated hue in both themes.
2026-07-15 15:15:38 +00: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
Claude bb9f0c0cb3 fix(concord): render imeta-only chat images and send thumbhash
Concord/NIP-C7 image messages from interop clients (e.g. Ditto/Soapbox
Armada) attach the image purely as a NIP-92 `imeta` tag and leave the
message content empty. Amethyst's shared renderer only shows media whose
URL appears in the text, so those images never rendered — even though the
encrypted blob's key/nonce were already registered for transparent
decryption at ingest.

RenderChat now appends any imeta URL missing from the content before
handing it to the media renderer (symmetric to ChannelChat.imageMessage,
which appends the URL on send), so imeta-only attachments display.

Also forward the thumbhash on the encrypted-image imeta: FileHeader
computes both blurhash and thumbhash, but only blurhash was being sent.
encryptedImageImeta now emits an additive `thumbhash` field and
toConcordImeta forwards the computed value, so receivers can paint a
placeholder while the blob decrypts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TGjBiJL2fvTR37eePBKrG
2026-07-15 14:54:48 +00:00
Claude df313970b1 refactor: extract bottom-bar settings state, share the entry resolver, read-only picker
Quality follow-up to the customizable bottom nav, addressing the audit's three
highest-impact items — behavior unchanged.

- Extract BottomBarSettingsState + the pure BottomBarEditing transforms out of
  BottomBarSettingsContent, so pin/unpin/reorder/restore-default are unit-tested
  (BottomBarSettingsStateTest) instead of only exercisable through the drag UI.
  The composable now just renders and forwards events; moveTransient reorders
  mid-drag and commit() persists once on drag end.
- Unify the phone bottom bar and the navigation rail on one rememberBottomBarSlot
  resolver (route + icon per entry), removing the duplicated built-in/favorite/
  group branches and the divergent selection logic that had drifted between them.
- Give the group resolvers a `subscribe` flag: the live bar/rail keeps a REQ open
  per pinned group (bounded by the few slots), but the settings picker reads
  cached metadata only — so expanding a chat category with many joined groups no
  longer fans out into one relay subscription per row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mxdSFQcarsKaL179tSub1
2026-07-15 14:53:20 +00: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
Claude f8f8dd59c2 fix: use non-deprecated PersistentMap.putting()/removing() in RelayAuthenticator
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Myhus2x1c3BSWCtjenSmkf
2026-07-15 14:38:39 +00: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
Claude ed0df81ccd feat: replace Messages FAB speed-dial with a full-screen conversation chooser
The Messages "+" button used to fan out four cryptic one-word FABs
(Private / Public / Group / Find groups). The labels didn't explain what
each protocol does, and two conversation types (Disappearing Chat and
Concord communities) had no entry point at all.

Replace the speed-dial with a single "+" that opens a new full-screen
"Start a conversation" chooser. It groups the six conversation types into
three intent buckets — Direct & private, Encrypted groups, Public & open —
and presents each as a card with an icon, a one-line tagline, a "Best for"
hint, and short pros/cons lists, so users can pick the right message
protocol for what they're building. Each card routes to that type's
existing creation (or browse) flow.

- New Route.NewConversation + NewConversationScreen
- ChannelFabColumn simplified to a single button opening the chooser
- Adds the previously-unreachable Disappearing Chat and Concord entries
2026-07-15 14:31:39 +00:00
Claude 7506ca599e fix: resolve Kotlin compiler warnings in commons and amethyst
- PoWPublishQueue: use non-deprecated PersistentMap.putting()/removing()
- NappletBrokerTest: drop cast that can never succeed after assertIs
- PrivacyLockStateTest: remove redundant !! (smart-cast already non-null)
- MinichatScreen: drop unnecessary !! on smart-cast non-null Strings
- Concord screens: remove unnecessary safe calls on non-null ChannelEntity
  and ResponseBody, and the now-dead elvis fallbacks

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Myhus2x1c3BSWCtjenSmkf
2026-07-15 14:16:35 +00:00
Claude d12093ec51 feat: pick individual chats/groups and favorites for the bottom nav bar
Rework the Bottom Navigation Bar settings picker so users can pin specific
joined chats — not just the aggregate list screens.

- Add PublicChat, RelayGroup and Concord variants to BottomBarEntry (stable
  @SerialName discriminators), so a specific NIP-28 channel, NIP-29 relay group
  or Concord community can be pinned as its own tab. The bottom bar and the
  navigation rail resolve each to its avatar + chat/home route, live from the
  local cache via a shared GroupBottomBarEntries resolver.
- Redesign BottomBarSettingsScreen: the pinned bar stays a drag-reorderable
  section on top; the "Available" list is now grouped into ordered, collapsible
  categories (Main, Chats & Groups, You, Feeds, Apps & Web, Other) instead of
  the flat, scattered catalog order. Browser expands to your favorite apps, and
  each chat type expands to your joined groups, each child pinnable with a
  toggle.
- Curated category ordering lives in BottomBarCategories, covered by a test that
  asserts every catalog id is placed in exactly one category.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mxdSFQcarsKaL179tSub1
2026-07-15 13:57:55 +00: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
Claude c00bd5bc27 feat: style Messages room-type labels as Concord-style pills
Render the "Public Chat" / "Marmot Group" (and the other room-type)
labels in the Messages list as a muted HeaderPill chip — the same faint
rounded chip the Concord community label uses — instead of plain gray
inline text, so every group kind reads the same way across the screen.
Each type gets a fitting icon (Public, Timer, Lock, Dns, Group).

Also rename the "MLS Group" label to "Marmot Group".
2026-07-15 13:43:28 +00: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
vitorpamplonaandgithub-actions[bot] 46f0a84f80 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-15 12:16:57 +00: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
davotoula 73044746c8 update cs,sv,de,pt 2026-07-15 11:42:37 +01:00
nrobi144andClaude Opus 4.8 3f56d177d8 feat(desktop): note scheduling + NIP-37 opt-in draft sync
Adds note scheduling and NIP-37 opt-in encrypted draft sync to Amethyst
Desktop, and extracts the existing Android scheduled-post code into
`commons` so both platforms (and PowJobRestorer) share one implementation.

- Compose → clock icon → date/time picker (presets + exact-minute); the
  note is pre-signed and stored locally, then published at its time.
- Publishes while the app is open (45s in-app tick + launch catch-up) AND
  while fully closed: an OS job (launchd / schtasks / systemd, registered
  only while the queue is non-empty) relaunches the binary in a headless,
  key-free `--publish-scheduled` mode that opens a websocket and pushes the
  pre-signed bytes.
- A "Scheduled" deck destination (tabs Scheduled / Drafts / Articles):
  status, cancel, publish-now, edit (cancel + reopen prefilled).
- Drafts: save-as-draft with a default-OFF "Sync across devices
  (encrypted)" toggle publishing a NIP-37 DraftWrapEvent (kind 31234,
  NIP-44 to self); drafts sync down on a fresh device.

Extraction / de-dup: ScheduledPost → commons/commonMain; ScheduledPostStore
+ ScheduledPostPublisher → commons/jvmAndroid (Jackson/java.io.File are
gate-forbidden in commonMain). The commons store is a strict superset of
upstream's parallel Android store (account-scoped claim, CLAIM_TTL crash
recovery, PUBLISHING-only status guards, reload-before-claim); upstream's
new ScheduledPostWorkGate gating is adopted to drive it. Single-writer file
lock + reload-before-claim so the in-app timer and headless process never
double-publish. Store file 0600, dir 0700.

macOS verified on the packaged app-image (compose+schedule, in-app publish,
app-closed launchd firing, Scheduled screen, NIP-37 draft round-trip).
Windows/Linux OS-integration authored but untested; headless has no Tor
routing yet — both documented in the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:26:12 +03: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
Claude b0d18b2983 fix(concord): import kotlin.jvm.JvmInline so ConcordPermissions compiles on iOS
`kotlin.jvm.*` is a default import on the JVM target but not on Kotlin/Native,
so `@JvmInline` on the ConcordPermissions value class resolved on JVM/Android
yet failed the iOS (compileKotlinIosSimulatorArm64) build with "Unresolved
reference 'JvmInline'". Add the explicit import; JVM is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-15 02:29:18 +00:00
Claude a47fd206e6 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt
#	cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt
2026-07-15 02:12:18 +00: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
Claude 90ab864362 test(marmot): make test_17 assert the image commit via a post-image message
The rename-echo assertion was flaky: firing two GCE commits (set-image + rename)
3s apart races whitenoise's per-epoch processing, so the second commit sometimes
lands as "unprocessable" and the name never propagates — a false negative.

The interop itself is confirmed working: in a harness run whitenoise's own log shows
`background_sync_group_image_cache_if_needed` parsing amy's image extension and
attempting a Blossom fetch (404 only because the harness sets the image without
uploading a blob), and test_09 (whitenoise messaging in the same group) passes right
after the image commit — both impossible if the image extension had been rejected.

Rewrite test_17 to the robust form: A sets the image, then sends ONE application
message at the post-image epoch; whitenoise can only decrypt it if it applied the
image-bearing commit, so wait_for_message B receiving it is direct proof the extension
parsed. Also skip cleanly when A is no longer a member of the group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JL3GXW1fmHa3xWfQjLLqfp
2026-07-15 01:58:29 +00:00
Claude a06cdd82fe feat(concord): rich, clickable relay rows in the community create/edit form
The Concord create/edit screens showed each community relay as a bare URL string
with a remove button — far more basic than every other relay list in the app.
Extract a shared ConcordRelayListEditor that renders each relay the way the Relay
Settings / Marmot screens do: the relay's NIP-11 favicon, its advertised name, and
its host, with the row tapping through to the full relay-info page
(Route.RelayInfo) and long-press copying the URL. Both screens now call the one
editor (also removes the duplicated relay block).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-15 01:54:32 +00:00
Claude b37a0ae3bb feat: add FAB to create a public chat on the Public Chats screen
Adds a "+" FloatingActionButton to the Public Chats screen (reached from
the left drawer) that navigates to the NIP-28 channel-creation flow
(Route.ChannelMetadataEdit with no id), mirroring the create-public-chat
action already offered by the Messages FAB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDKFHhUqcDJvNEA7s7Qspm
2026-07-15 01:44:21 +00:00
Claude d0a817a607 style(concord): drop bold top-bar titles to match the app's default weight
The shared TopBarWithBackButton renders its title as a plain Text (Material3's
default title weight), and screens like the Marmot group list follow that. The
Concord screens (and the minichat screen added alongside them) hardcoded
FontWeight.Bold on their TopAppBar titles, standing out from every other screen.
Remove the bold so the nav bars read consistently; content emphasis (unread
markers, names, section headers) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-15 01:42:01 +00:00
Claude e4c52a5553 test(marmot): refresh interop harness for restructured whitenoise-rs
whitenoise-rs became a workspace (core `whitenoise` crate at root, wn/wnd moved to
crates/whitenoise-cli) and grew native --discovery-relays / --default-account-relays
flags, which broke the headless harness's patch/build assumptions.

- Repath the mock-keyring patch to crates/whitenoise-cli/src/bin/wnd.rs and match the
  new main() (env-gated Whitenoise::initialize_mock_keyring_store(); the fn is pub
  under the integration-tests feature).
- Drop the discovery-env / defaults-env patches: start_daemon now passes the native
  --discovery-relays / --default-account-relays flags instead.
- Build wn/wnd via `cargo build -p whitenoise-cli --features whitenoise/integration-tests`
  (the binaries left the root crate; integration-tests brings in the mock keyring).
- The skip-unprocessable-retry patch still applies (root crate, 19-line offset).

Enables the end-to-end test_17_group_image_commit (amy sets a group icon -> whitenoise
must still process the commit) added in the previous commit to actually run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JL3GXW1fmHa3xWfQjLLqfp
2026-07-15 01:21:40 +00:00
Claude 22582d6a97 feat(cli): amy group set-image/clear-image + whitenoise interop regression test
Add `amy marmot group set-image <gid> <file> [--server URL]` and
`amy marmot group clear-image <gid>`: encrypt the avatar with the MIP-01 v2 scheme
(interoperable with mdk/whitenoise), optionally push the ciphertext to Blossom signed
by the keypair derived from image_upload_key, and commit the image fields into the
group's NostrGroupData extension. Thin assembly over quartz
(MarmotGroupImageEncryption) + commons (updateGroupMetadata) per the CLI rules.

Add headless interop test_17_group_image_commit: A (amy) sets a group image on a group
whitenoise is a member of, then renames. The rename is a later MLS epoch, so wn can
only observe the new name if it first applied the image-bearing GCE commit — proving
the image extension stays parseable on mdk (which rejects trailing bytes). This is the
end-to-end guard for the group-icon interop fix.

Note: the marmot interop harness currently can't build current whitenoise-rs — its
patches target the pre-restructure `src/bin/wnd.rs` layout, but whitenoise-rs has moved
to a `crates/whitenoise-cli/` workspace. Refreshing those patches is separate harness
maintenance; the interop correctness is otherwise covered by the pinned-mdk source
analysis and the MarmotGroupImageTest wire regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JL3GXW1fmHa3xWfQjLLqfp
2026-07-15 01:07:00 +00:00
Claude 3950323ba3 fix(concord): audit fixes — memory, folding, concurrency, notifications, UI
Address the deep-audit findings across the new Concord code:

- H1: stop persisting kind-21059 ephemeral typing wraps as durable notes.
  EphemeralGiftWrapEvent extends GiftWrapEvent, so every heartbeat was
  stored forever; drop it once the session has ingested it.
- H2: follow()/unfollow() now read the offline backup (entriesWithBackup),
  so a join racing the async backup load can no longer wipe the joined list.
- M1 (banlist): fold to the head (honors a chained unban) then union in
  authorized editions that aren't ancestors of the head — concurrent bans
  are healed without resurrecting an on-chain unban (CORD-06 down-only).
- M2: reproject only the newly-arrived channel wrap incrementally instead
  of re-decrypting the whole buffer per message (was O(n^2)); refold only
  projects newly-folded channels.
- M3: cancel a session's old state-watcher before replacing it on a
  Refounding rebuild (was a coroutine + session leak per rekey).
- M4: publish typing/state/members/observed-authors under the lock and
  make revision/observedAuthors updates atomic; clamp future-dated typing.
- M5: notification Concord bypass now requires the community to be one this
  account has currently joined (mirrors the Marmot guard).
- M6: Concord chat honors the "Messages in notifications" toggle.
- L1: carry NIP-30 emoji tags on minichat replies, image captions and
  custom-emoji reactions.
- C1: make the composer VM init() idempotent so recomposition can't wipe a
  picked image or an open suggestion list.
- C2/C3: ConcordHome channel rows and unread badges react to the channel's
  own notes flow instead of the global revision (no stale rows / flicker).
- C4: try/finally around mint-invite / create / save so a thrown call can't
  strand the button disabled.
- C5: gate the typing ticker on active heartbeats so an idle channel stops
  waking a 2s loop.

Adds regression tests for concurrent-ban union-heal and unauthorized bans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-15 00:37:13 +00:00