Commit Graph
648 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub 085e2466e6 Merge pull request #3221 from nrobi144/fix/relay-health-threading-and-sleep-resume
fix: RelayHealthStore threading + sleep-resume socket recovery (follow-up to #3186)
2026-06-16 08:40:37 -04:00
Vitor Pamplona 7b0e8b7117 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-06-15 10:46:15 -04:00
Vitor Pamplona 0cfc324d87 Removes Threading checks on Commons since Main Threads don't exist over there.
Remove warnings
2026-06-15 10:41:40 -04:00
nrobi144 b91519157e fix(commons,quartz): RelayHealthStore threading + sleep-resume socket recovery
Follow-up to #3186, addressing the unresolved review feedback:

- RelayHealthStore.schedulePersist() wrapped the blocking save() in withContext(ioDispatcher)
  so prefs.flush() no longer sits on the Compose composition thread on Desktop.
- close() now fires the final save on a detached IO-bound scope instead of blocking
  the composition thread for ~50ms during account switch / app exit.
- @Volatile on persistJob/tickJob and a closed-flag guard so the relay-network thread
  and composition thread no longer race on plain vars (and post-close work is dropped).
- desktopApp/Main.kt passes Dispatchers.IO to RelayHealthStore so persistence flushes
  land on the IO dispatcher instead of Dispatchers.Default.

Plus a separate-but-related fix to the offline-banner-stuck-after-Mac-sleep issue:
NostrClient.keepAliveJob now tracks wall-clock overshoot of its scheduled tick.
If the OS suspended us (laptop lid closed, system sleep), delay() returns far
past its deadline and the OkHttp websockets we held are dead even though
BasicRelayClient.isConnected() still reads true until the next ping fails.
On a >5x interval overshoot, force relayPool.disconnect() + connect() instead
of trusting needsToReconnect(), so feeds resume without an app restart.
2026-06-15 13:21:32 +03:00
Claude b565c37277 fix: parse IPv6-literal URLs in post content
UrlParser.parseValidUrls filtered every detected URL through
isValidTopLevelDomain(), which requires the TLD's first character to be
an ASCII letter. IPv6 literal hosts are bracketed (e.g. [2001:db8::1])
and have no dotted TLD, so the whole bracketed host became the candidate
"TLD", starting with '[' and failing the check. As a result, valid
IPv6 URLs like http://[302:68d0:f0d5:b88d::bdb]/<hash> were dropped and
rendered as plain text instead of links.

The UrlDetector already validates the bracketed address as syntactically
correct IPv6, so accept bracketed hosts directly in isValidTopLevelDomain.
2026-06-14 15:59:32 +00:00
Vitor PamplonaandGitHub fcb4b7a9ad Merge pull request #3202 from vitorpamplona/claude/sweet-shannon-smjotq
Redesign activity cards (reactions, zaps, nutzaps) with unified UI
2026-06-13 11:28:38 -04:00
Claude 1709d5a30a fix: replies to likes/zaps no longer pull in the liked post's thread
Reactions and zaps keep the post they target in Note.replyTo so the card
can embed it, but that edge bridges into a different conversation. The
anchorsItsOwnThread guard only covered clicking the like/zap itself, not
clicking a kind-1111 reply to one — so opening such a reply's thread had
searchRoot/loadUp climb reply -> like -> liked post -> ... -> root of the
liked post's thread, dropping the reply into the wrong conversation, and
replyLevel buried it several indents deep.

Treat reactions/zaps as thread boundaries everywhere reposts already are:
- ThreadAssembler.searchRoot stops at a reaction/zap node.
- ThreadAssembler.loadUp adds the node but does not climb its replyTo.
- ThreadLevelCalculator (replyLevel + replyLevelSignature) treats them as
  level-0 roots.

The boundary predicate is promoted to a shared Event?.anchorsItsOwnThread().
The data layer (replyTo/replies/computeReplyTo) is unchanged — card
rendering and the notification relevance filter still rely on the link;
only thread traversal stops crossing it.

https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
2026-06-13 14:57:46 +00:00
Claude 74d394a7f7 Merge remote-tracking branch 'origin/main' into claude/beautiful-ride-n6y3s2
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
2026-06-12 22:52:57 +00:00
Claude 46b2147598 Merge remote-tracking branch 'origin/main' into claude/sweet-shannon-smjotq 2026-06-12 19:52:34 +00:00
Claude 0948c0d467 feat: reactions and zaps anchor their own thread; drop chip long-press
Opening the thread screen on a reaction, lightning zap, nutzap, or onchain
zap now shows that event as the root with only its reply subtree below it —
the post it targets stays visible through the card's embedded preview, but
the target's conversation no longer loads around it. Replies and comments
(kind 1 / 1111) keep the existing behavior: the full parent thread loads
and the screen lands on the clicked note. Covered by ThreadAssemblerTest.

The long-press reply shortcut on notification zap chips is removed: the
thread view's reply button is now the single reply entry point (and still
routes private zaps to the sender's DM room via routeReplyTo).

https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
2026-06-12 19:44:09 +00:00
Vitor PamplonaandGitHub 1927ce1ef0 Merge pull request #3191 from vitorpamplona/claude/elegant-allen-9mt4he
Unify payment card UI with PaymentCard component
2026-06-12 10:59:49 -04:00
Vitor PamplonaandGitHub b37e62cd62 Merge pull request #3186 from nrobi144/feat/unhealthy-relay-review
feat(desktop): unhealthy-relay review banner + popup
2026-06-12 09:31:34 -04:00
davotoula a593ec35f4 Code review:
- Account.kt: collapse three identical backend-not-configured Failure
  constructions into one helper
- OnchainZapSendError: declare causeIsUserFacing on the enum so the
  sender owns which failures carry a human-readable cause, instead of
  the UI mapper hardcoding the list
- SendPaymentScreen: fee chip label is now a single format resource
  instead of manual string concatenation
2026-06-12 14:05:12 +02:00
davotoula 97f9cca43e feat(l10n): localize onchain zap send dialog and failure messages 2026-06-12 14:04:55 +02:00
nrobi144 a46a72a89f feat(desktop): unhealthy-relay review banner + popup
Surfaces relays unresponsive for 7+ days across the user's NIP-65 (10002),
DM (10050), and Search (10007) relay lists. A non-modal banner appears
above feed columns (and above the single-pane content) whenever the
classifier finds anything; tapping it opens an anchored Popup with one
row per unhealthy relay and per-row Remove / Open Dashboard / Snooze 7d
actions plus a banner-level "Snooze all 7d".

Quartz
- RelayStat gains best-effort lastConnectAt + lastIncomingAt timestamps
  (epoch seconds, 0 = never observed). RelayStats listener pushes them
  on onConnected / onIncomingMessage. Durable per-relay history lives
  outside quartz in the commons RelayHealthStore.

Commons (new commons/relays/health/ package)
- classifyRelayHealth() pure function with the v1 gates:
  * first-run grace (don't flag for 7d after firstScanAt)
  * offline grace (don't flag if no relay anywhere has responded)
  * Tor-mode skip (relay timing is intentionally lossy through Tor)
  * per-relay snooze (snoozedUntil > now)
  * 10006 (blocked) excluded from detection but still part of the
    multi-list Remove action
- RelayHealthStore (account-scoped, supervised scope, 5s debounced
  persist, 60s ticker for snooze expiry).
- RelayHealthListener wires the quartz lifecycle into the store.
- RelayHealthPersistence interface (no expect/actual — single impl per
  platform via injection).
- RelayListMutator interface + RelayRemovalResult sealed type.
- Shared UnhealthyRelayBanner (errorContainer @ 50% alpha) and
  UnhealthyRelayRow (static outlined tag chips, no ripple) composables.
- 8 classifier unit tests covering each gate + multi-list membership.

Desktop wiring
- PreferencesRelayHealthPersistence (java.util.prefs.Preferences, per
  account via 8-char pubkey prefix).
- DesktopRelayListMutator runs the 4 sign-and-broadcast jobs in
  parallel via async/awaitAll so a slow NIP-46 bunker doesn't multiply
  latency by 4.
- Banner placed in DeckColumnContainer + SinglePaneLayout, store +
  listener + per-account scan trigger wired in Main.kt's MainContent.

Scope: Desktop only for v1. Android wiring is intentionally not in
this PR — the commons module is platform-neutral and ready for Android
to follow whenever someone wants to pick it up.
2026-06-12 06:40:46 +03:00
Claude 275c53ad7b feat: modernize inline payment cards and surface their descriptions
Redesign the three payment cards rendered in the middle of a post
(Lightning invoice, CLINK Offer, Cashu token) around a shared PaymentCard
scaffold that follows the wallet screens' Material3 idiom: tonal card,
icon + label header with a copy action, centered headline amount, and a
full-width themed Pay/Redeem button (no more hardcoded white text or
7sp mint lines).

Descriptions were not being rendered at all:
- BOLT-11: LnInvoiceUtil only decoded the amount from the HRP. Add
  tagged-field parsing (description 'd', expiry 'x', timestamp) with
  BOLT-11 spec-vector tests; the invoice card now shows the memo and
  flags expired invoices (Pay disabled). Desktop card shows it too.
- Cashu: V3 'memo'/'unit' and V4 'd'/'u' were parsed then dropped.
  CashuToken now carries them; the card shows the memo and no longer
  mislabels non-sat units (usd/eur cents formatted as decimals).
- CLINK Offers: the card now shows who gets paid (avatar + name from
  the pointer's pubkey, tappable to the profile).

https://claude.ai/code/session_019VuZ4y3ij6Ly4VVExE1W1W
2026-06-12 00:05:30 +00:00
Claude 96eaefda9b fix: tie rumor host lifetime to the Note — replace the RumorHosts index
A memory audit found the global strong-reference RumorHosts index could
never be kept in sync with LocalCache.notes, which holds WeakReferences:
seven prune paths (pruneExpiredEvents — rumors inherit the seal's
expiration tag — hidden/old-message/replaceable/reaction/hidden-event
prunes, and cleanMemory) plus silent GC eviction dropped rumor notes
without clearing their entries, clear() had no callers (logout, account
removal, memory trim), and orphaned stubs accumulated unbounded.

The stub now lives on the Note (Note.rumorHost): whatever removes or
garbage-collects the note frees the stub, closing every leak path by
construction. Cost is one nullable reference per Note (~200-400 KB at a
50k-note steady state) versus the index's per-entry map overhead plus
unbounded orphan growth. All consumers already held the Note: toNEvent,
Account.broadcast, deleteEnvelopes, removeIfWrap, chat pruning, and the
ingestion pipeline. RumorHosts is deleted.

Also fixes the desktop regression the audit surfaced: the desktop
gift-wrap handler now records the wrap on the rumor note, so desktop
nevent citations of chat messages point at the wrap id again instead of
exposing the private rumor id.

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 23:35:25 +00:00
Vitor PamplonaandGitHub d91988b26b Merge pull request #3185 from vitorpamplona/claude/focused-einstein-6jjqmj
Add unified profile payment screen with multi-rail support
2026-06-11 19:14:43 -04:00
Vitor PamplonaandGitHub 1e23b14ff2 Merge pull request #3184 from vitorpamplona/claude/beautiful-turing-j0czsm
Add NIP-101e fitness workout support (Kind 1301)
2026-06-11 18:26:36 -04:00
Claude 3e9fce1858 refactor: replace WrappedEvent host tracking with the RumorHosts index
Delivery metadata no longer lives on quartz event classes. The mutable
host var on @Immutable events (with its dual @Transient annotations) is
gone, and any event kind can now be a rumor without subclassing anything
— kind-14 chats and kind-1 private replies use one mechanism.

- commons RumorHosts: rumor id → delivering envelope (the kind-1059
  wrap normally, a bare kind-13 seal otherwise), populated by the
  gift-wrap ingestion pipeline from the publicNote threaded through the
  handlers (the seal's host pointer was never needed)
- Note.toNEvent cites the envelope for ANY rumor — this also fixes
  kind-1 private replies, whose nevent previously exposed the private
  rumor id (kind-14s were already wrap-cited)
- Account: rumorHost() reads the index; relay computation refuses
  seals, inner DM messages, and unsigned rumors explicitly
- LocalCache: deleteWraps → deleteEnvelopes (also removes the seal
  layer the old host-chain walk missed); removeIfWrap and chat-history
  pruning read the index; index entries are dropped with their rumor
- quartz: SealedRumorEvent and BaseDMGroupEvent extend Event directly;
  GiftWrapEvent.unwrap no longer injects host stubs; WrappedEvent
  deleted

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 22:18:50 +00:00
Claude 4361f95a17 feat: NIP-101e workout records (kind 1301) + Workouts feed screen
Quartz: new experimental/fitness/workout package shaped like nip88Polls —
WorkoutRecordEvent with per-tag classes (exercise, duration, distance,
elevation, calories, steps, heart rate, splits, strength sets/reps/weight,
source, workout_start_time), TagArrayBuilder/TagArray extensions, lax
RUNSTR-dialect parsing (unit defaults, HH:MM:SS or raw seconds), and
EventFactory + LocalCache registration. Covered by fixture tests.

Amethyst: new Workouts feed (drawer entry, route, follow-list top bar,
per-relay filter assemblers mirroring the Pictures feed) with a + FAB
opening a manual workout composer that publishes canonical kind-1301
events. Workout cards render stats chips and also display inside threads
via NoteCompose. Adds fitness Material Symbols glyphs and regenerates the
subset font.

https://claude.ai/code/session_01Kpx53UEeJqqR7CASzMu6GB
2026-06-11 21:48:32 +00:00
Claude 02e0d9a4be feat(profile): pay bitcoin payment targets through the in-app on-chain wallet
Lightning payment targets already route into the Send Payment screen;
this extends the same treatment to bitcoin targets. Tapping a profile's
bitcoin payment-target chip (or its pay action in the wallet-button
dialog) now opens the Send Payment screen with the on-chain rail locked
to that announced address, paid directly from the user's NIP-BC Taproot
wallet — falling back to the external bitcoin: URI when the chain
backend is missing or the address isn't a payable native-segwit mainnet
address.

- quartz: SegwitAddress.scriptPubKeyFor/isPayableMainnetAddress;
  OnchainZapBuilder.buildToScripts core shared by the pubkey paths.
- commons: OnchainZapSender.sendToAddress — plain wallet send with the
  same fund-safety signing contract but no kind:8333 receipt (the
  destination isn't pubkey-derived, so none is possible); the signing
  block is now a single shared helper across send/sendSplit/sendToAddress
  and Success.receiptEventId is nullable for receipt-less sends.
- amethyst: Account.sendOnchainToAddress; Route.SendPayment gains
  btcAddressOverride; a shared inAppPaymentRouteFor() decides which
  payment targets the user's wallets can pay in-app (used by both the
  target chips and the payment-targets dialog).
- Send Payment screen: with an address override the on-chain rail shows
  the target address, hides the message field (no receipt to carry it),
  explains that no zap receipt is published, and dispatches the plain
  address send.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 21:01:07 +00:00
Claude fa25f22f5f Merge remote-tracking branch 'origin/main' into claude/beautiful-ride-n6y3s2 2026-06-11 20:54:50 +00:00
Claude dd9ee0b5c6 fix: close audit findings — report leak, model-level rumor guards, hide share/bookmarks on private notes
Merge-readiness audit follow-ups:

- Account.report(note): reporting a private rumor now reports the AUTHOR
  (p-tag only) instead of publishing a kind-1984 that e-tags the private
  rumor id onto public relays (the one confirmed leak)
- Defense-in-depth guards at the model layer so the invariant no longer
  relies on UI gating alone: RepostAction.repost returns null / throws
  for empty-sig targets (covers Account.boost, createBoostEvent, and the
  desktop call path), ReactionAction.reactTo (simple overload) throws,
  and Account.broadcast no-ops for unsigned non-wrapped events — without
  the guard it would disclose the rumor JSON to relays even though they
  reject the signature
- Hide remaining actions that can't work on private rumors, per review:
  share buttons (action row, both note menus) and all bookmark/playlist/
  emoji-list rows (their lists reference an id other devices can't
  resolve; public lists would also leak it)
- ZapCustomDialog: remember(accountViewModel, baseNote) so the
  preselected zap type can't go stale on lazy-list slot reuse

Broadcast of the gift wrap itself (like DMs do via WrappedEvent.host)
needs host tracking for non-WrappedEvent rumor kinds in quartz — left
as a follow-up; the broadcast row stays hidden for kind-1 rumors.

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:54 +00:00
Claude 7e7d8e5325 feat: private reactions on unsealed rumors + leak prevention for private notes
Unsealed NIP-59 rumors (private replies/posts arriving in gift wraps
from other clients) were indexed as ordinary notes: public reactions,
reposts, edits, pins, OTS timestamps, labels, public bookmarks, and
deletion requests could all e-tag the private rumor id onto public
relays.

- Note.isPrivateRumor(): empty-signature discriminator (rumors are the
  only notes materialized with an empty sig; draft inners are never
  indexed as standalone notes)
- ReactionAction: reactions inherit the target's privacy — empty-sig
  targets get gift-wrapped kind-7s fanned to the rumor author, every
  tagged user, and the sender's self-copy (add-only; un-react would
  need a public NIP-09 deletion that leaks the rumor id)
- AccountViewModel.reactToOrDelete: never NIP-09-delete rumor reactions
  (also fixes the same leak for existing NIP-17 chat reactions),
  tracked-broadcast mode excluded for rumor targets
- ReactionsRow: hide reply/boost/zap on private rumors (each publishes
  a public e-tag of the target); like stays, now wrapped
- DropDownMenu/NoteQuickActionMenu: hide broadcast, edit, timestamp,
  pin, hashtag label, public bookmarks, deletion request for rumors;
  private bookmarks and block/report stay available
- Lock badge in the note header (reuses existing Lock glyph, no font
  regen needed)

Covered by ReactionActionTest (public vs rumor fan-out, jvmTest green).
Plan: commons/plans/2026-06-10-private-replies-reactions-posts.md

https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
2026-06-11 20:53:51 +00:00
Claude 73a63cf43a fix: address audit findings on the MLS unread/restore changes
- Clamp the seeded kind:445 subscription since at wall-clock now: the
  inner createdAt is sender-controlled, so a single future-dated message
  could push since past the present and silently skip genuinely new
  events on every restart. Covered by a new regression test.

- Drop the remember() around the group-list unread count: the chatroom's
  message set can shrink without newestMessage or lastReadTime changing
  (pruning, kind:5 deletion of an older message), which left the cached
  count stale. The set is pruned to ~100 entries, so counting per
  recomposition is cheap.

- Extract marmotGroupLastReadRoute(): the "MarmotGroup/<id>" last-read
  key was inlined at three call sites; a prefix drift between the
  mark-as-read side and the unread checks would silently reintroduce
  the bug this branch fixes.

- Derive GROUP_EVENT_REFETCH_OVERLAP_SEC from TimeUtils.ONE_DAY instead
  of re-deriving 24*60*60.
2026-06-11 20:05:25 +00:00
Claude 438f37a1ad Merge remote-tracking branch 'origin/main' into claude/kind-lamport-dwtzh8 2026-06-11 19:00:39 +00:00
Claude d242eb62aa Merge remote-tracking branch 'origin/claude/trusting-mayer-6o0yd5' into claude/trusting-mayer-6o0yd5 2026-06-11 18:18:57 +00:00
Claude 990c5afe99 Merge remote-tracking branch 'origin/main' into claude/trusting-mayer-6o0yd5
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt
2026-06-11 18:15:04 +00:00
Vitor PamplonaandClaude Opus 4.8 f9f7de3ed0 feat(tor): money-operations relay category
Relay-socket Tor routing only had localhost/onion/DM/trusted/new buckets,
so a wallet or payment-service relay fell through to newRelaysViaTor and
got forced over Tor regardless of the "Money operations via Tor" toggle
(which previously governed only HTTP clients). On services that block Tor
exits this silently broke NIP-47 and CLINK payments.

Add a moneyOperationsViaTor field to TorRelaySettings and a moneyOpRelay
bucket to TorRelayEvaluation (taking precedence over DM/trusted/new, after
the onion reachability check). TorRelayState gains a persistent money-op
relay set — fed across all accounts from NIP-47 wallet relays and saved
CLINK debit relays via AccountsTorStateConnector — plus a reference-counted
ad-hoc registry for one-off payment relays (e.g. an noffer pointer). The
websocket builder resolves the per-relay decision from live source values
so ad-hoc registration takes effect on the next connect with no race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:35:11 -04:00
Claude 2eb6510eec fix: stop refetching the full MLS kind:445 backlog on every restart
The Marmot subscription since, the processed-event dedup set, and the
application ratchet position (group state persists only at commits) are
all in-memory only. On restart, relays therefore redeliver the group's
entire kind:445 history and the rewound ratchet re-decrypts old
application messages as if they had just arrived — wasted decryption
work and, when a replay beats the disk restore, duplicate entries
appended to the persisted plaintext message log.

Two defenses:

- MarmotManager.restoreAll() now seeds each restored group's
  subscription since from the newest persisted decrypted message, minus
  a one-day overlap window for late/out-of-order publishes. Seeding
  happens before syncWithGroupManager registers default entries, so
  even the first filter set sent to relays carries it. The CLI is
  unaffected: it builds group filters from its own persisted since.

- MarmotMessageStore appends are now explicitly idempotent (contract
  was previously ambiguous and both real stores appended blindly):
  the Android and CLI file stores skip an entry that is already in the
  group's log, so replays inside the overlap window cannot grow it.

Covered by MarmotManagerRestoreTest in commons jvmTest — placed there
rather than androidHostTest because CI only runs :commons:jvmTest (the
androidHostTest task currently fails on android.util.Log stubs even
for the pre-existing Marmot test).
2026-06-10 23:03:48 +00:00
Claude af7d61f361 fix: persist MLS chat unread state across app restarts
The unread dot for Marmot/MLS group rooms was driven by an in-memory
unreadCount on MarmotGroupChatroom. On restart the MLS group state is
restored from the last persisted commit, the kind:445 subscription
restarts with since=null, and the in-memory processed-event dedup set is
empty — so relays redeliver old group events, they re-decrypt as fresh
application messages, and the counter was re-bumped, resurrecting the
dot for chats already read.

Marking-as-read was already persisted: opening a group chat writes the
newest rendered message's createdAt to the MarmotGroup/<groupId> route
in lastReadPerRoute (saved to disk with account settings). Compute the
unread indicators from that timestamp instead — exactly how DM rooms
and public channels do it — in both the Messages screen row and the
Marmot group list row.

With no consumer left, drop the volatile counter and collapse the
addMessageSync/restoreMessageSync split (they only differed in the
counter bump).
2026-06-10 22:08:51 +00:00
Claude 84e91833c4 feat: add @-mention user search and tagging to Marmot group chat composer
Brings the MLS/Marmot message composer to parity with NIP-17 DMs:
typing @ shows the shared user-suggestion dropdown (local cache +
NIP-05 resolution), selecting a user inserts @npub…, and on send
NewMessageTagger rewrites mentions into nostr: URIs and collects
the referenced users as p-tags on the inner kind:9 rumor. Mentions
stay inside the MLS ciphertext; the outer kind:445 is unchanged.

Also applies MentionPreservingInputTransformation and
UrlUserTagOutputTransformation to the field so mentions render
highlighted while composing, matching the DM editor.

https://claude.ai/code/session_013NWdjCSegsf2FYSPPANX3n
2026-06-10 15:35:03 +00:00
Claude 84497bbd47 perf(commons): index URLs by first char in fixMissingSpaces
fixMissingSpaces runs on the main thread once per rendered note. The scan
introduced in the previous commit re-tested every detected URL at every
character position (and allocated an iterator per position via firstOrNull),
i.e. O(N*U*L) for a note with U URLs — noticeable on large notes that carry
many links.

Bucket the URLs by their first character once up front and only attempt a
match at positions whose character can actually start a URL; every other
character now costs a single map lookup, keeping the pass linear in the text
length for typical content. Buckets stay longest-first so prefix URLs still
don't shadow longer ones, so the output is identical (verified against the
commons richtext JVM corpus).
2026-06-09 22:52:00 +00:00
Claude f42cc836aa fix(commons): make fixMissingSpaces work on Kotlin/Native (iOS)
RichTextParser.fixMissingSpaces used a Regex of the form
`([^ \n])?(urls)([^ \n])?` to insert spaces around URLs glued to
neighbouring text. Kotlin/Native's regex engine fails to backtrack the
optional `([^ \n])?` capture groups to zero width, so on iOS every URL was
corrupted (e.g. "https://x" became "h https://x"). That broke the
downstream segmenter, which is why :commons:iosSimulatorArm64Test reported
19 failures across the RichText/Gallery/Pdf/F4a parsers once the test binary
finally linked.

Replace the regex with a direct left-to-right scan that inserts a single
space wherever a detected URL touches a non-space/non-newline neighbour. The
scan is engine-independent, so it behaves identically on JVM and Native.
Verified equivalent to the old behaviour across the full commons richtext
JVM corpus, and the new FixMissingSpacesTest pins the cases on every target
(including iosSimulatorArm64).
2026-06-09 22:42:50 +00:00
Claude f0276f1e04 feat(clink): debit payment-source model + unified default resolver
Adds the verifiable core for using a CLINK debit pointer as a spend rail
alongside NWC:
- ClinkDebitWalletEntry (commons): a saved ndebit pointer, the spend-only
  counterpart of NwcWalletEntry (no secret, no balance/history)
- PaymentSource + PaymentSourceResolver (commons): unifies NWC wallets and
  CLINK debits into one list with a single default id spanning both types;
  no explicit default falls back to first (NWC before debits), preserving
  today's behavior. canShowBalance marks NWC vs debit honestly.
- ClinkDebitPayer (amethyst): publishes the kind-21002 pay request and awaits
  the preimage via a one-shot subscription, mirroring ClinkOfferPayer.

Resolver logic covered by PaymentSourceResolverTest on JVM (7 cases incl.
cross-type default + stale-id fallback); amethyst compiles. Persisting the
new fields in AccountSettings and the Wallet-screen rows/confirm dialog are
the next (compile-only) step.
2026-06-09 21:52:05 +00:00
Claude 62e522ccc4 feat(clink): detect noffer pointers in rich-text as ClinkOfferSegment
Teaches the commons RichTextParser to recognize an inline noffer1...
token and emit a ClinkOfferSegment carrying the decoded NOffer, so a
GUI front end can render a 'Pay' card in the note body (the feed-offer
feature). Bare tokens only for now; nostr:/lightning: prefixed forms
fall through. Covered by ClinkOfferSegmentTest on JVM.
2026-06-09 20:58:58 +00:00
Claude 85f9998463 revert: drop ChatroomList.changes flow and the screen's reactive use
Backs out the reactive list-refresh plumbing added in the prior commit:
removes the `changes` SharedFlow from the shared `ChatroomList` (restoring
it to its original form) and reverts Desktop `ChatroomListState` to its
original 2s poll. Room assembly is expected to move to a LocalCache.observe
approach on both platforms later, which would supersede this.

Keeps the independent Desktop list improvements (per-room unread tracking
and the mute/acceptable filter), which don't depend on the flow.

https://claude.ai/code/session_01VEukNczAYxNLBjLnqVEoZd
2026-06-09 20:57:45 +00:00
Claude 1fbefb2efe feat(desktop): bring group DM parity with Android
Makes the Desktop DM client a first-class group participant and tightens
the shared/Desktop DM paths so they match Android behavior.

- commons ChatNewMessageState: actually attach the composed NIP-14 subject
  to sent messages (the field was previously collected but dropped).
- commons ChatroomList: emit a `changes` SharedFlow on add/remove so list
  UIs can refresh reactively; dedupe the User overloads onto the room ones.
- Desktop NewDmDialog: multi-recipient selection (chips + confirm button) so
  a Desktop user can start a group, not only a 1:1.
- Desktop ChatPane/ChatroomHeader: show a group's NIP-14 subject in the
  header and add a rename dialog that broadcasts a subject change to all
  members.
- Desktop Main.kt DM ingest: route any ChatroomKeyable inner event into the
  room (covers kind 14/15 and future variants) and store self-authored
  NIP-37 drafts instead of dropping them.
- Desktop ChatroomListState: refresh reactively off ChatroomList.changes
  (with a slower safety poll), track real per-room unread via a last-seen
  mark, and hide rooms whose latest message isn't acceptable (mute/filter).

https://claude.ai/code/session_01VEukNczAYxNLBjLnqVEoZd
2026-06-09 20:16:55 +00:00
Vitor PamplonaandGitHub 3dfe418150 Merge pull request #3151 from vitorpamplona/claude/relay-message-pagination-LMqSQ
DM history: per-relay backward paging, live-tail split + prune-aware window realignment
2026-06-09 15:05:58 -04:00
Vitor PamplonaandClaude Opus 4.8 39eb25bc17 fix(commons): show "N relays" on every history marker count chip
The in-stream loading marker spelled out "N relays" only for the fully-loaded
(done) chip; active frontiers showed a bare count ("Loading: ↓ 8"). Use the
relays plural for the count fallback on every state so a count chip always reads
as a sentence ("Loading: ↓ 8 relays"). 1–2 short host names still spell out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:17:07 -04:00
davotoula 711ca547b5 fix(math): render inline equations wrapped in opening punctuation 2026-06-09 19:51:31 +02:00
Vitor Pamplona b21aecdfca Improves markers 2026-06-09 13:47:46 -04:00
Claude d6603baf1a Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-09 12:54:51 +00:00
Vitor PamplonaandGitHub 8afdfba1c2 Merge pull request #3153 from vitorpamplona/claude/equation-rendering-klegT
fix(math): collapse over-escaped backslashes in inline equations
2026-06-08 20:57:56 -04:00
Claude be06b7335b fix(math): collapse over-escaped backslashes in inline equations
Some sources (e.g. the math-academy posts) over-escape their LaTeX, so
the content carries `\\ldots` / `\\cdots` instead of `\ldots` / `\cdots`.
JLaTeXMath reads the `\\` as a forced TeX line break — splitting every
inline equation across two lines — and renders the trailing command name
as the literal letters "ldots"/"cdots", so the ellipsis symbol is lost.

Collapse doubled backslashes (`\\cmd` -> `\cmd`) before rendering inline
math; display math is left untouched since `\\` can be a genuine line
break there. No-op when the content isn't over-escaped.
2026-06-09 00:52:47 +00:00
Vitor PamplonaandGitHub dd88ae3167 Merge pull request #3152 from vitorpamplona/claude/equation-rendering-klegT
Add LaTeX math rendering for $...$ and $$...$$ equations
2026-06-08 20:36:11 -04:00
Vitor PamplonaandClaude Opus 4.8 aa6b9bc53e refactor(dm): centralize the DM history-window boundary + log prune rewinds
The "where the live tail ends and paged history begins" boundary (one week)
was copy-pasted across the gift-wrap + NIP-04 live tails, the backward history
pager floor, and the prune's recent/old split — easy to drift out of lockstep
(overlap = double-load, gap = missed messages).

- Add DmHistoryTuning: one place for liveTailSeconds + recentKeepCount, read by
  AccountGiftWrapsEoseManager, both NIP-04 SubAssemblers, BackwardRelayPager,
  and Chatroom.pruneMessagesToTheLatestOnly. Drops the duplicated
  LIVE_TAIL_SECONDS / DEFAULT_LIVE_TAIL_SECONDS constants.
- Log the prune-time window realignment under DMPagination, per scope
  ([giftwrap] / [rooms.nip04] / [convo.nip04]): relay count + newest pruned
  timestamp, so the rewind is observable in logcat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:59:04 -04:00
Vitor PamplonaandClaude Opus 4.8 96ff5316dc fix(dm): realign the per-relay download window when DMs are pruned
Memory pruning drops DM messages out of the cache but left the per-relay
paging cursors untouched, so a relay still claimed to have delivered the
dropped band (reachedUntil deep, or done) and the demand-driven loader
never re-requested it — a silent hole until app restart.

- Prune NIP-17 too: pruneMessagesToTheLatestOnly now reaps both NIP-04
  (PrivateDmEvent) and NIP-17 (WrappedEvent rumors) on one merged top-N
  cut, so a conversation is cut at a single time point (no NIP-04-without
  -NIP-17 holes). NIP-17 is the actual memory-pressure driver.
- HostStub carries the host's createdAt, so a decrypted rumor self-
  describes its outer gift-wrap time (the time the cursor pages by; the
  rumor's own time is the message time, not the wrap time).
- RelayLoadingCursors.rewindTo() pulls a relay's reached cursor up past
  the pruned band, clears done, and un-arms it (demand-driven re-fetch);
  advance() now resumes from the rewound reached point instead of the
  floor.
- LocalCache.pruneOldMessages accumulates the newest pruned created_at
  per relay (outer-wrap time for gift wraps, event time for NIP-04),
  filtered below each cursor's floor, then rewinds giftWrapHistory +
  rooms-list nip04History (account-wide) and the per-conversation
  nip04History.

The gift-wrap window is account-global, so pruning one room rewinds the
shared sweep; the interference is bounded (already-held wraps short-
circuit in consumeRegularEvent, re-fetch is demand-gated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:03:50 -04:00
Claude 1e76705c87 Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ
# Conflicts:
#	commons/src/commonMain/composeResources/values/strings.xml
2026-06-08 18:15:56 +00:00