Commit Graph
17041 Commits
Author SHA1 Message Date
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
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 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
Claude 6735c0b7a6 fix(commons): import kotlinx.coroutines.IO for native Dispatchers.IO access
`BlossomServerListState` used `Dispatchers.IO` without importing the
common `kotlinx.coroutines.IO` accessor. On JVM/Android the JVM member
resolved fine, but on Kotlin/Native (iosSimulatorArm64) it resolved to
the internal `Dispatchers.IO` member and failed to compile:

    Cannot access 'val IO: CoroutineDispatcher': it is internal in
    'kotlinx.coroutines.Dispatchers'.

Add the missing `import kotlinx.coroutines.IO`, matching every other
common-source state holder in this module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtPgFEy9AxoaUT5AqEFBZX
2026-07-16 01:10:31 +00:00
Claude 389d750ccc fix(concord): don't hang forever on an unopenable invite link
Redeeming a Concord invite link (`…/invite/<naddr>#<fragment>`) that was
minted by a newer/incompatible Concord client left the redeem screen on an
endless "Redeeming invite…" spinner with a retry button that could never
succeed. The bundle event fetches fine, but its content can't be decrypted
(e.g. a bundle tagged `vsk=8` vs the `vsk=6` this app mints/reads), so
`ConcordInviteBundle.parse` returns null and the old `String?` contract
collapsed every failure into a single generic "could not fetch" + retry.

`joinConcordViaInvite` now returns a `ConcordInviteResult` that separates the
outcomes — joined, invalid link, not reachable (transient), and a bundle that
was found but couldn't be opened (incompatible/newer). The invite screen shows
a specific message per case and only offers a retry for the transient miss, so
an invite this app can't open ends on a clear explanation instead of looping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KngFNwrQDLYa9QW1f5RRD
2026-07-16 00:49:15 +00: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
Claude 275df2ae9c fix(desktop): drain kind:10002 back-fill fully and close on EOSE
Audit follow-ups on the DM relay-list work:

- Bug: scheduleOutboxBackfill re-scheduled itself from inside the still-active
  drain job, so the isActive guard made the tail call a no-op — any authors
  past the first 100-author batch in a burst were stranded until another kind:0
  happened to arrive. Drain in a while-loop inside one job instead, and mark it
  @Synchronized so concurrent consume-path callers can't spawn duplicate jobs.
- Perf: the back-fill held each REQ open for a fixed 8s. Use fetchAll, which
  returns on EOSE (bounded by the timeout), and reuses existing infra.
- DmInboxRelayResolver: drop the redundant `+ cachedOutbox` in the phase-2 seed
  (cached outbox is already in phase1Seed, so it was always subtracted back
  out), and bound the phase-2 fallback fetch to 5s so a cold send can't stack
  two full fan-out timeouts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSc3LhGFSF5VZKr9h3qfn3
2026-07-16 00:31:18 +00:00
Claude 2d311c9324 Merge remote-tracking branch 'origin/main' into claude/nip17-dm-relay-desktop-4shxdy
# Conflicts:
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt
2026-07-16 00:20:28 +00:00
Claude 931236e719 feat(desktop): back-fill kind:10002 on profile load and read DM inbox from outbox relays
Answers "do we load kind:10050 from each user's outbox write relays?" — now we
do, and we make sure we know where those relays are.

- DmInboxRelayResolver.resolve is now two-phase: query the curated indexers ∪
  the recipient's known write relays (via a new outboxLookup) for 10050/10002;
  if the indexers didn't carry the 10050, read it directly from the write
  relays learned from their kind:10002 — the canonical NIP-65 outbox location.
  Wired on desktop with cachedAdvertisedRelayList(pubkey).writeRelaysNorm().
- Whenever DesktopLocalCache ingests a user's kind:0, it fires
  onProfileMetadataConsumed; the subscriptions coordinator back-fills that
  user's kind:10002 (batched ≤100 authors/REQ, deduped, one REQ at a time) and
  routes it through consume() so it's saved. So learning who a user is now also
  learns where they write.

Tests: DmInboxRelayResolverOutboxTest covers reading 10050 from the recipient's
write relays (indexers only have their 10002) and the cached-outbox seed path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSc3LhGFSF5VZKr9h3qfn3
2026-07-16 00:15:28 +00:00
vitorpamplonaandgithub-actions[bot] 2b30a85dce chore: sync Crowdin translations and seed translator npub placeholders 2026-07-16 00:13:55 +00: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
Claude 152fc76bc0 fix(desktop): fetch account config (incl. blossom) from outbox relays
The account-config bootstrap subscription only queried the default relays,
so a user's Blossom server list (kind 10063) — published to their own write
relays, not the defaults — was never fetched, and the UI fell back to the
default server. NIP-65 relay lists hid the same gap because they're broadcast
widely and also have a local backup.

Add a subscription that re-fetches the account-config kinds (10002/10050/
10007/10006/10063) from the user's NIP-65 outbox (write + untagged relays)
once it's known, routing kind 10063 / 10002 through justConsumeMyOwnEvent
like the bootstrap does. This matches mobile's outbox model: the user's own
data comes from their write relays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dkzkEY6cUsRfqEb7giHi2
2026-07-16 00:03:38 +00: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
Claude c5ad932c1b feat(desktop): make DM relay-list prewarm viewport-driven, not a full sweep
The first pass warmed every conversation peer on disk from the 2s refresh loop.
Scope it to what the user is actually looking at instead:

- Move the prewarm to a reusable, deduped, concurrency-capped
  DesktopIAccount.prewarmDmInboxRelays(pubkeys) any DM surface can call.
- Trigger it per-row from the conversation list's LazyColumn, which only
  composes visible rows (+ buffer), so peers warm as their row scrolls into
  view and no earlier — scrolling warms more.
- Warm a recipient the moment they're picked in the New DM dialog, so the
  kind:10050 is ready before the composer opens.
- Room open already resolves peers via ChatNewMessageState.load().

ChatroomListState.prewarmPeerRelays now just delegates to the account.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSc3LhGFSF5VZKr9h3qfn3
2026-07-15 23:54:07 +00:00
Claude a347889c57 fix(nip29): audit fixes for pinned-message jump and back-fill
- Jump effect: key on the note id alone and always clear the request
  after one attempt. Previously keyed on (id, items.list), so a message
  arriving mid-jump cancelled the scroll animation and could leave the
  request stuck (never cleared when the target wasn't loaded), blocking
  repeat taps. Now the animation can't be interrupted and a re-tap always
  re-fires.
- Back-fill pinned bodies: add an id-scoped filter for the group's
  pinnedEventIds on the host relay. A pin can point at a message older
  than the 200-item timeline window, which the h-scoped timeline never
  returns — without this the bar shows a blank preview and can't jump.
- Re-invalidate the relay-group filter when the pin list changes (keyed
  on the pin ids only, so roster/metadata churn doesn't re-subscribe), so
  the new pin ids are actually requested when 39005 arrives.
2026-07-15 23:52:20 +00:00
Claude f727c226c0 refactor(desktop): provide blossom servers via LocalBlossomServers
Replace the threaded blossomServers parameters with a LocalBlossomServers
CompositionLocal, provided once from the account's state holder
(iAccount.blossomServerList.flow) around the logged-in UI. Upload sites read
the list from context instead of receiving it down a parameter chain.

- Provide LocalBlossomServers in MainContent's existing provider (covers
  feeds, chats, profile, settings) and around the top-level compose dialog.
- ComposeNoteDialog, EditProfileDialog, ChatPane and the media-server
  settings section read LocalBlossomServers.current; drop the params and the
  prop-drilling through UserProfileScreen and DesktopMessagesScreen.
- This also covers quote-compose from a feed row (NoteActionsRow), which the
  parameter approach couldn't reach — the per-note card composables now get
  the list from context, so it no longer defaults to the primal server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dkzkEY6cUsRfqEb7giHi2
2026-07-15 23:44:49 +00: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
Claude 2eaaa270c3 refactor(desktop): read blossom servers from the account, not the cache
Replace the ICacheProvider.blossomServers(pubKey) cache helper with reads
straight from the account's own state holder — iAccount.blossomServerList.flow
(the shared BlossomServerListState) — threaded to each upload site. This is
the reactive, per-account source of truth and drops the cache+pubkey
indirection entirely.

- Hoist iAccount (with dmSendTracker + accountRelays) out of MainContent into
  the LoggedIn branch so the top-level compose dialog can read the account's
  blossom flow too; pass them into MainContent as params.
- Thread iAccount.blossomServerList.flow into ComposeNoteDialog (reactive:
  the server picker updates if the list loads after the dialog opens),
  EditProfileDialog (via UserProfileScreen), and ChatPane (via
  DesktopMessagesScreen).
- Reduce BlossomServers.kt to just the DEFAULT_BLOSSOM_SERVER fallback used
  when the account has published no kind-10063 list yet.

Known gap: quote-compose opened from a feed row (NoteActionsRow) still
defaults to DEFAULT_BLOSSOM_SERVER — the per-note card composables don't
carry the account handle, and threading it through the whole note-render
tree isn't worth it for that secondary path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dkzkEY6cUsRfqEb7giHi2
2026-07-15 22:51:57 +00:00
Claude ca01e98d60 fix(chat): drop the dead footer gap on messages with no timestamp or chips
A chat bubble reserves ~8dp beneath its last line of text so the reaction
chips, which overlap the bottom border, ride over padding instead of the
text. That reserve was gated on `reactionsRow != null`, but the reaction row
was passed for every non-inner-quote message regardless of whether it had any
engagement — so every footerless bubble (the middle messages of an author
run, which don't show the timestamp) reserved space for an empty chip row,
leaving a visible gap between the text and the bubble's bottom edge.

Gate the chip row on actual engagement via a new `hasChatEngagement` helper
that mirrors the render gate in `RenderChatReactionChips` (reactions, zaps,
pending zaps, minichat replies) and keeps the same live subscriptions, so a
message that gains its first reaction still mounts the row. Footerless
messages with no engagement now hug their text; the reserve returns only when
chips are actually present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ghZ7egU86LT4Z5BBvyLvY
2026-07-15 22:34:31 +00: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
Claude b8966049b8 fix: Concord channel rows stuck on "No messages yet" despite having messages
consumeConcordRumor attaches a message row to its ConcordChannel BEFORE
justConsume loads the event (so the note carries its gatherer through the
Messages filter). That means Channel.addNote ran while createdAt() was still
null and never set lastNote — and on reprojects the containsKey guard skips
addNote entirely, so lastNote stayed null forever for every Concord channel.
The list/hub rows read lastNote, so they always showed "No messages yet" (and
activity sorting was a no-op) even after messages had loaded.

Adds Channel.refreshAfterEventLoad(note), called right after justConsume once
the event is present: it picks lastNote when newer and invalidates the notes
flow so previews, unread counts, and ordering recompute. Regression-tested in
commons (ConcordChannelLastNoteTest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9Uv3h6GhSQVuUTY5Ffm6e
2026-07-15 21:59:25 +00:00
Claude 1896d77bd9 refactor(desktop): drop the global blossomServers pref, read per-account
The kind-10063 list is already the per-account source of truth in the
cache, so the DesktopPreferences.blossomServers singleton was redundant —
and being process-global (not per-account) it was also a latent
account-switch bug: the mirror could hand one account's media servers to
another.

Remove it and have every consumer read the account's list from the cache:

- Add ICacheProvider.blossomServers(pubKey) / preferredBlossomServer(pubKey)
  helpers (+ DEFAULT_BLOSSOM_SERVER fallback).
- Upload paths read per-account: ComposeNoteDialog (localCache), ChatPane
  (its cacheProvider), EditProfileDialog (localCache threaded from
  UserProfileScreen).
- Settings screen falls back to the default constant instead of the pref;
  drop the flow→prefs mirror LaunchedEffect.
- Delete DesktopPreferences.blossomServers / preferredBlossomServer.

Uploads require network anyway, by which point the account-config
subscription has loaded kind 10063, so no local persistence is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dkzkEY6cUsRfqEb7giHi2
2026-07-15 21:40:54 +00:00
vitorpamplonaandgithub-actions[bot] 65d16d185b chore: sync Crowdin translations and seed translator npub placeholders 2026-07-15 21:25:50 +00:00
Claude 9ee6f0dbde feat(nip29): message pinning for relay groups
Implements NIP-29 message pinning (nostr-protocol/nips#2379):

Protocol (quartz):
- GroupPinnedEvent (kind 39005): relay-signed pinned-message list, `d`
  group id + ordered `e` ids.
- UpdatePinListEvent (kind 9010): moderator `update-pin-list` write,
  carries the full list so pin/unpin/reorder/clear are one submission.
- Register both in EventFactory; add a pinnedEventIds tag helper.

Model + cache:
- RelayGroupChannel now folds the pin list (pinnedEventIds / isPinned)
  with the same createdAt-supersede guard as the roster.
- LocalCache consumes 39005 into the channel and stores the 9010 write;
  39005 added to the group's metadata REQ filter so pins load.

Publish path:
- Account.pin/unpin/updateRelayGroupPins + AccountViewModel wrappers.

UI (non-intrusive, self-hiding):
- Collapsed pinned-message bar under the top bar: shows the current pin,
  N-count cycling, tap to jump to the message in-feed (hoisted jump
  request threaded through the shared chat feed view). Renders nothing
  when the group has no pins.
- Moderator-only Pin/Unpin action under "Show more" in the chat message
  bottom drawer, gated on membership.canModerate().
- Small pin glyph on pinned bubbles' footer.

Tests: quartz build/parse round-trip for both kinds; channel pin-fold
supersede/replace/clear semantics.
2026-07-15 21:24:37 +00:00
Claude dbb8cc522e feat: make Concord channel rows feel alive — facepile, live typing, animated badge
Builds on the last commit to push the server channel list beyond a plain
messaging list:

- Recent-poster facepile: a stack of overlapping avatars (newest on top) for
  each channel's latest distinct authors, so a busy channel reads as "who's
  here" at a glance. New ConcordAuthorFacepile reuses the existing avatar
  primitives; recentAuthorHexes() picks the most-recent distinct posters in one
  O(notes) pass.
- Live "X is typing…": each row consumes the community session's typing
  heartbeats (kind 23311) and swaps its preview line for an italic typing
  indicator, driven by one screen-level freshness ticker that only spins while a
  heartbeat is live.
- Rows are now a two-line layout (name + facepile / preview + time + badge), and
  the unread badge animateContentSizes as its count changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9Uv3h6GhSQVuUTY5Ffm6e
2026-07-15 21:24:25 +00:00
Claude 7ecb1af13b feat: enrich Concord channel list rows with last message + unread counts
The community "server" channel list (ConcordChannelListScreen) showed only an
icon and name per channel. It now mirrors the chat-room list feel: each row
carries a preview of the last message (author + snippet), the relative time of
that message, and an unread-message count badge, with the name/icon/time
emphasized while unread.

Adds a shared, reactive unread-message counter (concordChannelUnreadCountFlow)
that combines each channel's persisted last-read marker with its own note store,
plus a shared ConcordUnreadBadge pill (capped at 99+, plural content
description). The Concord Channels hub (ConcordHomeScreen) now reuses both:
per-channel rows show the real new-message count instead of a bare dot, and a
community's badge sums new messages across its channels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9Uv3h6GhSQVuUTY5Ffm6e
2026-07-15 21:24:25 +00: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
Claude 0d58d94ef4 fix: resolve CI lint error and coroutines opt-in warnings
- Collect ConcordSession.state via collectAsStateWithLifecycle instead of
  reading StateFlow.value in composition (fixes StateFlowValueCalledInComposition
  lint error in GroupBottomBarEntries.kt).
- Add @OptIn(ExperimentalCoroutinesApi::class) for testScheduler.advanceTimeBy
  and runCurrent usages in ResourceUsageLedgerTest and RelayAuthPromptBusTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx8SvzsWsru7rEQ1KcbSCW
2026-07-15 21:13:23 +00:00
Claude 5c858a3a29 feat(desktop): download NIP-17 DM relay lists proactively and back the User model
The desktop app resolves each recipient's kind:10050 DM inbox (strictly, no
NIP-65 fallback) when sending NIP-17 messages and reactions, but only lazily
at send/room-open time via the indexer fan-out — and recipient relay lists
that arrived through the normal feed were being dropped, so the cache never
helped.

- Route kind:10050 (ChatMessageRelayListEvent) through DesktopLocalCache.route
  into addressableNotes, mirroring kind:10002. Previously route() dropped it.
- Back the User model's pinned replaceable notes (kind 10002/10050/10019) with
  the same addressableNotes map that consume writes into. Before this, the
  desktop UserContext read a plain notes map that nothing populated, so
  User.dmInboxRelaysStrict()/outboxRelays() always returned null — leaving the
  DM inbox resolver's local lookup, the send-path fallback, and tier-1 AUTH for
  the user's own DM relays effectively dead.
- Consume the user's own kind:10050 into LocalCache at bootstrap alongside its
  persisted accountRelays copy.
- Prewarm each conversation peer's DM relay list as soon as their room appears
  in the list (concurrency-capped, deduped), so the first send/reaction
  resolves from cache and the composer's "no DM relays" gate settles early.

Adds DesktopDmRelayListConsumeTest covering kind:10050 routing, newer/older
replacement, and the unknown-recipient case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSc3LhGFSF5VZKr9h3qfn3
2026-07-15 20:59:10 +00:00
Claude e322c939cc feat(desktop): load Blossom servers from kind 10063 like mobile
The desktop app read its Blossom media server list only from a local
DesktopPreferences string (defaulting to blossom.primal.net) and never
looked at the user's NIP-B7 BlossomServersEvent (kind 10063) — the same
event the Amethyst mobile app loads via BlossomServerListState. A server
list configured on mobile therefore never showed up on desktop.

Load the list from the network event instead, mirroring the existing
desktop NIP-65 flow:

- Add a shared, platform-agnostic BlossomServerListState in commons that
  reads the kind-10063 addressable event from ICacheProvider and exposes
  a StateFlow<List<String>> plus a save helper.
- Store incoming kind-10063 events in DesktopLocalCache.route()
  (consumeBlossomServerList, newest-per-author wins).
- Instantiate blossomServerList on DesktopIAccount and subscribe to
  kind 10063 in the account-config bootstrap subscription.
- Mirror the loaded network list into DesktopPreferences so the upload
  path and cold start reflect it; the network event stays authoritative.
- Feed the media-server settings screen from the network list and, on
  edit, sign+broadcast a new kind-10063 event so changes sync to every
  Amethyst client (writeable accounts only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dkzkEY6cUsRfqEb7giHi2
2026-07-15 20:56:57 +00: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
vitorpamplonaandgithub-actions[bot] 6139923561 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-15 20:10:21 +00:00
Claude 284d7b608a Merge remote-tracking branch 'origin/main' into claude/modernize-chat-rendering-kigcsh
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt
2026-07-15 20:08:42 +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 56e3ab5b8b feat(concord): stamp the wrap's seen-on relays onto the decrypted rumor
Like NIP-17's addRelayToNoteAndInners, propagate the relays a Concord plane
wrap was seen on down to its inner rumor, so a received Concord/Armada
message shows the relays it actually came from — not just the channel's
configured relay set.

The wrap note already carries its seen-on relays (added by consumeRegularEvent
before the gift-wrap handler runs), so GiftWrapEventHandler hands them to
concordSessions.ingest, which threads them through the session/registry to the
rumor sink; LocalCache.consumeConcordRumor then stamps them onto the rumor note
after justConsume. Local-echo sends and buffer re-projections pass no relays
(default empty) since there's no per-wrap relay to attribute there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
2026-07-15 20:00:47 +00:00