Commit Graph
757 Commits
Author SHA1 Message Date
Claude f26c00add0 refactor(commons): make HtmlParser KMP — drop java Charset dependency
Follow-up to the link-preview move: HtmlParser + HtmlCharsetParser were
stuck in jvmAndroid only because they spoke java.nio.charset.Charset.
There is no common Charset type in the Kotlin stdlib, so this reshapes
the API to speak IANA charset *names* (String) and pushes the single
genuinely-platform operation — byte->String decode — behind expect/actual.

- Move HtmlParser + HtmlCharsetParser to commonMain. Charset detection
  (meta-tag sniff + BOM sniff) is pure string/byte work; BOM detection no
  longer needs okio (manual leading-byte compare).
- Add `expect fun decodeBytes(bytes, charsetName)`:
    * jvmAndroid actual -> java.nio.charset (all JRE charsets, UTF-8 fallback)
    * iosMain actual -> NSStringEncoding for the common web charsets
      (UTF-8/16/32, Latin-1, CP1252, ASCII), UTF-8 fallback for the rest.
- UrlPreview (stays jvmAndroid; needs OkHttp) now reads response.body.bytes()
  and passes mimeType.charset()?.name().

Verified: commons compiles for JVM AND iosSimulatorArm64, verifyKmpPurity
passes, commons jvmTest passes, amethyst play + fdroid compile.
2026-05-30 23:34:05 +00:00
Claude 58ad87c900 refactor(commons): move link-preview fetcher to commons
Third slice of the amethyst→commons migration. UrlPreview (OpenGraph
link-preview fetcher) and HtmlParser already wrapped the extracted
commons preview parsers (MetaTagsParser/OpenGraphParser/HtmlCharsetParser);
this consolidates the whole link-preview concern in commons.

- Move service/previews/{UrlPreview,HtmlParser} into commons jvmAndroid
  service preview package. They land in jvmAndroid (not commonMain)
  because UrlPreview uses OkHttp and HtmlParser uses java.nio.charset —
  both JVM-only. No Android-framework or keystone coupling: the caller
  injects the OkHttpClient as a lambda.
- Add explicit okhttp + okhttp-coroutines deps to commons jvmAndroid
  (previously only present transitively via coil-okhttp).
- Re-point the single caller (model/UrlCachedPreviewer).

commons JVM compile + verifyKmpPurity pass; amethyst play + fdroid compile.
2026-05-30 22:57:37 +00:00
Claude 5f1514a152 refactor(commons): move relay broadcast tracker to commons
Second slice of the amethyst→commons migration. BroadcastTracker +
BroadcastEvent/RelayResult/BroadcastStatus are platform-agnostic relay
event-broadcast logic (no keystone coupling, no Android) that Desktop and
the CLI can reuse.

- Move service/broadcast/{BroadcastModels,BroadcastTracker} into
  commons commonMain service/broadcast.
- Replace the two commonMain purity-gate violations:
  System.currentTimeMillis() -> TimeUtils.now() (startedAt is only used
  to sort the active-broadcast list) and java.util.UUID.randomUUID() ->
  RandomInstance.randomChars(16) for the tracking id.
- Re-point the 4 Android callers (AccountViewModel + broadcast UI).

verifyKmpPurity passes; amethyst play + fdroid both compile.
2026-05-30 22:42:05 +00:00
Claude e66c27e375 refactor(commons): move CLI-safe util extensions out of amethyst
First low-friction slice of the amethyst→commons migration
(commons/plans/2026-05-30-amethyst-to-commons-migration.md): the model
nipNN state holders are all blocked by the LocalCache/Note/Account
keystone (Phase A), so start with the genuinely Android-free utilities.

- Delete amethyst service/IterableExt.kt — exact duplicate of the existing
  commons util/IterableUtils.kt (Iterable.replace); re-point 4 callers.
- Move retryIfException (CoroutinesExt.kt) into commons util/CoroutinesUtils.kt.
- Move togglePresenceInSet (SetExt.kt) into commons util/SetUtils.kt.

All commonMain-safe (verifyKmpPurity passes). amethyst play + fdroid both
compile against the relocated helpers.
2026-05-30 22:23:47 +00:00
Vitor PamplonaandGitHub 4ae606805f Merge pull request #3112 from vitorpamplona/claude/gracious-cori-uLr4P
Move NIP-51/72 decryption caches and models to commons
2026-05-30 18:04:49 -04:00
Claude abc9cd14cf refactor: move InterestSet to commons
Pure data class (only @Stable) — extract to
commons/model/nip51Lists/interestSets so Desktop/CLI/iOS can reuse the
interest-set model. Re-points the four interest-set UI files and the
sibling InterestSetsState.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:59:26 +00:00
Claude a38388fce5 refactor: move LabeledBookmarkList to commons
Pure data class (only @Stable + quartz bookmark tags) — extract to
commons/model/nip51Lists/labeledBookmarkLists so Desktop/CLI/iOS can
reuse the bookmark-group model. Re-points the six bookmark-group UI
files and the sibling LabeledBookmarkListsState.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:52:06 +00:00
Claude 593c320004 refactor: move Mute/People/Community decryption caches to commons
Extract the three remaining keystone-free quartz-only decryption caches
(MuteListDecryptionCache, PeopleListDecryptionCache,
CommunityListDecryptionCache) into commons/model so Desktop/CLI/iOS can
reuse them. Re-points Account, FeedDecryptionCaches, and the sibling
state holders (MuteListState, PeopleListsState, BlockPeopleListState,
CommunityListState).

The remaining relay-list decryption caches depend on
GenericRelayListCache -> amethyst.model.Note (the keystone), so they
stay until Phase A extracts Note/LocalCache.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:46:13 +00:00
Claude ab72427445 refactor: move HashtagListDecryptionCache to commons
Pure quartz-only decryption cache — extract to
commons/model/nip51Lists/hashtagLists. Re-points Account,
FeedDecryptionCaches, and the sibling HashtagListState.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:43:55 +00:00
Claude 466086c475 refactor: move FavoriteAlgoFeedsListDecryptionCache to commons
Pure quartz-only decryption cache — extract to
commons/model/nip51Lists/favoriteAlgoFeedsLists so Desktop/CLI/iOS can
reuse it (mirrors the TrustProviderListDecryptionCache move). Re-points
Account and the sibling FavoriteAlgoFeedsListState.

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:42:51 +00:00
Claude c619d71890 refactor: move NwcWalletEntry to commons/model/nip47WalletConnect
Pure data class (only quartz + a UUID id generator) — extract to
commons so Desktop/CLI/iOS can reuse the NWC wallet entry model.
Swaps java.util.UUID for the multiplatform kotlin.uuid.Uuid (matching
the quartz convention) and re-points the three callers
(LocalPreferences, AccountSettings, WalletViewModel).

https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb
2026-05-30 21:40:54 +00:00
Vitor PamplonaandGitHub 34f6b2ba36 Merge pull request #3111 from vitorpamplona/claude/epic-hamilton-23225
NIP-32: Add hashtag labeling and label-based hashtag feed
2026-05-30 17:32:03 -04:00
Claude b0f9b1621a refactor: move CashuToken to commons/model/nip60Cashu
CashuToken and Proof are pure data classes (only @Immutable +
kotlinx.serialization). Extract to commons so Desktop/CLI/iOS can reuse
them, and re-point all callers.

https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod
2026-05-30 20:37:04 +00:00
Claude d57f8c18c6 feat: first-class NIP-32 hashtag labels on posts and in the hashtag feed
Let users tag any post with a hashtag via a NIP-32 kind 1985 label event
(using the `#t` tag-association namespace), and surface follow-labeled
posts in the hashtag feed.

quartz:
- LabelEvent.buildHashtagLabel() + HASHTAG_NAMESPACE ("#t") and
  hashtagAssociations() to build/extract hashtag-association labels.

commons:
- Note now carries a `labels` reverse-reference map (hashtag -> labeler
  notes) with addLabel/removeLabel and a NoteFlowSet.labels flow,
  mirroring reactions/reports.

amethyst:
- LocalCache consumes LabelEvent, attaching hashtag labels to their
  target notes and re-notifying feed observers for already-cached
  targets.
- Account.createLabelHashtagEvent/labelHashtag/consumeLabelEvent and
  AccountViewModel.labelWithHashtag (tracked + direct broadcast).
- Overflow "⋯" menu gains an "Add hashtag" action backed by a new
  AddHashtagLabelDialog.
- HashtagFeedFilter also accepts posts a followed user labeled with the
  hashtag; a new label sub-assembler subscribes to kind 1985 by `#l`
  and fetches missing label targets.
- Hashtag feed shows an attribution banner ("#tag added by @user") above
  follow-labeled posts via a custom RefresheableFeedView onLoaded.

https://claude.ai/code/session_019gc3FipVBcndF9fmqCCfVX
2026-05-30 20:32:43 +00:00
Claude 19d6c9baaa refactor: move TrustProviderListDecryptionCache to commons, rename pkg to nip85TrustedAssertions
Extract the pure (quartz-only) decryption cache into commons. Converge
the commons trustedAssertions package onto the quartz NIP slug
'nip85TrustedAssertions' (per migration plan §10.2), moving the existing
TrustProviderListState interface + UserCardsCache with it and
re-pointing all callers.

https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod
2026-05-30 20:15:36 +00:00
Claude 47a70b4fa3 refactor: move OwnedEmojiPack to commons
Pure data class (only quartz + @Stable deps) — extract to
commons/model/nip30CustomEmojis so Desktop/CLI/iOS can reuse it.
Moves the unit test to commons commonTest and re-points callers.

https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod
2026-05-30 20:15:28 +00:00
Claude 6b2265273a docs(commons): add target package hierarchy + naming rules for the migration
Adds the architecture end-state (ports exist, impls are forked 3 ways =>
unify), the concrete destination package tree for the migrated common
objects (cache engine, decomposed Account into model/account state +
actions + facade, per-NIP state holders, feeds, keystorage, service split),
and the package-naming decisions behind it (model/nipNN raw state vs
model/account composed view; state/ stays generic; actions=verbs; quartz
slug normalization; LocalCache class+delegating object). Also drops a stray
markup line at the end of the doc.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 19:51:38 +00:00
Claude d7aa307b9c docs(commons): add Amethyst→commons migration plan
Survey of amethyst/{model,service,ui} (1817 files) classifying what should
move to commons vs stay Android-native, with per-area matrices, the real
cross-cutting blockers (account state is the keystone; R.string is solved by
Compose Resources, not a new StringProvider; Coil is already KMP), a
dedup/reconciliation backlog, and a phased roadmap with a concrete first PR.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 19:36:22 +00:00
Claude d8899eedf8 refactor(commons): move feature-specific UI out of ui/ into <feature>/ui
Make the feature-UI vs cross-cutting-UI rule consistent (feature-first):
- ui/nip53LiveActivities -> nip53LiveActivities/ui
- ui/article + ui/editor -> new nip23LongContent/ui (article reader + editor)

ui/ now holds only cross-cutting composables (theme, components, layouts,
elements, markdown, signing, thread, feeds, notifications, screens, state,
text). Tighten ARCHITECTURE.md with the deciding test ('could a second
unrelated feature reuse this as-is?') and reconcile the NIP-second-axis
section so a single-NIP feature owns its UI under <feature>/ui rather than
ui/nipNN.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 19:02:55 +00:00
Claude 79a9bf78f8 refactor(commons): name single-NIP feature packages after their quartz NIP
Rename the two single-NIP feature packages to mirror their quartz
counterparts for 1:1 traceability:
- chess -> nip64Chess
- call  -> nipACWebRtcCalls

marmot and nip53LiveActivities already match quartz and are unchanged.
Document the rule in commons/ARCHITECTURE.md: layer is the primary axis,
NIP is the secondary axis (nipNN<slug> matching quartz), and commons is
deliberately NOT reorganized NIP-first at the top level. Also remove
stray markup that leaked into the end of the doc.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 18:46:36 +00:00
Claude b0c6ffb821 refactor(commons): consolidate package taxonomy + add architecture doc
Document the commons module's purpose, source-set layout, and the CLI-safe vs
UI boundary in commons/ARCHITECTURE.md, then clean up the clearest package
overlaps that had accumulated:

- merge duplicate util/utils -> util (all source sets)
- unify service/services -> service (jvmAndroid)
- move data/UserMetadataCache -> model/cache
- fold compose/ into ui/ (ui/article, editor, elements, layouts, markdown,
  nip53LiveActivities, and Compose helpers in ui/state + ui/text)
- move ProfileBroadcastBanner composable into profile/ui

All changes are whole-file/whole-package moves with import rewrites; no logic
changed. The chess logic/UI split is documented as deferred debt (it needs
file-level surgery, not moves). Marks docs/shared-ui-analysis.md superseded.

https://claude.ai/code/session_01KXLzsvx9Gyrm3Yz4Rims55
2026-05-30 17:03:57 +00:00
Claude 93b77348ac refactor: enlarge the Cashu sunglasses further
Bump the pixel sunglasses scale (1.45 -> 1.70) so the shades read bolder and
overhang the slimmed, rounded cashew body more prominently. Body outline and
1.2 stroke weight unchanged.
2026-05-29 23:30:48 +00:00
Claude df33b99b07 refactor: round the Cashu nut body into a smooth outline
Replace the blocky pixel-stepped cashew silhouette with a smooth rounded
outline (corner-cut and emitted as a compact Bézier spline) so the nut reads
as a clean curved shape instead of a staircase. Use a round stroke cap/join at
the same 1.2 weight; the pixel "deal-with-it" sunglasses stay a solid fill.
2026-05-29 23:01:34 +00:00
Claude 71bed1cf0a refactor: slim Cashu body and enlarge the sunglasses
Squeeze the cashew outline horizontally (~0.72) and scale the pixel sunglasses
up (~1.45) so the shades overhang the body for a bolder, more recognizable
mark. Stroke weight stays at 1.2 to match the shared Zap outline icon.
2026-05-29 22:51:54 +00:00
Claude 9010e85411 feat: monochrome outline Cashu icon for Material Symbols compatibility
Convert the multi-tone "deez nuts" Cashu/nutzap logo into a single-color,
tintable outline icon so it behaves like a Material Symbol glyph: the cashew
body is a hollow stroke (1.2 weight, matching the shared Zap outline icon) and
the pixel sunglasses stay a solid fill so they read at small sizes.

Drop the `tint = Color.Unspecified` overrides at every call site (zap chips,
nutzap rows/gallery, redeem, wallet screens) so the icon now tints with the
surrounding content colour instead of being locked to the brand browns, and
remove the imports/comments that only existed to preserve the old multi-tone
rendering.
2026-05-29 22:38:43 +00:00
Claude 30a845a6c1 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-amethyst-sdOWe
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
2026-05-29 13:32:49 +00:00
Vitor Pamplona c1dd59a068 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
# Conflicts:
#	commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/subscriptions/LifecycleAwareKeyDataSourceSubscription.kt
2026-05-29 08:57:31 -04:00
Vitor Pamplona c1c4f7f72a Uses the parent note's p-tags to load children notes that cannot be found 2026-05-29 08:47:28 -04:00
Vitor Pamplona ca8589b29c Quick check to make sure the gatherers are not duplicated 2026-05-29 08:37:08 -04:00
Vitor PamplonaandGitHub fd88e2f8a5 Merge pull request #3095 from vitorpamplona/claude/amazing-ptolemy-26Nek
Use locale-aware date/time formatting throughout the app
2026-05-29 08:30:41 -04:00
Vitor PamplonaandGitHub 9a6400a6e3 Merge pull request #3103 from greenart7c3/claude/elegant-knuth-grbfU
Fix lifecycle-aware subscription grace timer starvation
2026-05-29 06:34:53 -04:00
Claude 5667bd53c3 fix(relays): keep lifecycle-aware grace timer running while backgrounded
The grace-period unsubscribe in LifecycleAwareKeyDataSourceSubscription ran
on the composition scope from rememberCoroutineScope(), whose dispatcher is
coupled to the UI frame clock. When the app is backgrounded the frame clock
stops ticking, so the pending unsubscribe could be starved and never fire.
Because closing the REQ is what drives the relay disconnect (via desiredRelays
-> RelayPool.updatePool), the connection could linger indefinitely. This is
most visible on the relay feed, whose dedicated one-off relay is kept alive by
nothing else.

Drive the grace timer from Lifecycle.currentStateFlow on a dedicated
Dispatchers.Default scope instead. collectLatest cancels the pending delay
automatically when the lifecycle returns to STARTED, preserving the 30s
app-switch grace while ensuring the timer fires reliably in the background.

https://claude.ai/code/session_01SesftJphLwvLtn1fJB5zx8
2026-05-29 09:29:48 +00:00
nrobi144andClaude Opus 4.6 7ff4d8e3b7 fix(desktop): avatar ripple + P3 hardcoded colors + inline shapes
Avatar/Account switcher:
- Rewrite SidebarAccountHeader with same shape/hover as other nav items
- Entire row (avatar + display name) is clickable with rounded clip
- Inline DropdownMenu replaces overlaid AccountSwitcherDropdown
- Collapsed: compact avatar with same rounded hover treatment

P3 #004 — Hardcoded status colors:
- Add StatusGreen/StatusRed/StatusAmber to commons Colors.kt
- Replace 32 inline Color() values across 10 files with theme tokens
- Color.Red → MaterialTheme.colorScheme.error where appropriate
- Color.Green/Gray → StatusGreen/onSurfaceVariant

P3 #005 — Inline shapes:
- RoundedCornerShape(8.dp) → MaterialTheme.shapes.small (~20 files)
- RoundedCornerShape(12.dp) → MaterialTheme.shapes.medium
- RoundedCornerShape(16.dp) → MaterialTheme.shapes.large
- Pill shapes (100dp/999dp) kept as-is

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:52 +03:00
nrobi144andClaude Opus 4.6 b431c1efab feat(desktop): visual personality overhaul — unified theme, sidebar, cards
Phase 1: Replace per-OS color schemes with unified Amethyst brand
- Cyan/blue accent (#0096FF light, #4DB8FF dark) replacing OS-adaptive colors
- Amethyst purple as tertiary heritage color
- Unified shapes (8/12/16/24dp) replacing per-OS variants
- Standardized typography weights (Light for display, SemiBold for headlines)
- Letter spacing unified to -0.3sp

Phase 2: Spacing system
- AmethystSpacing CompositionLocal with design tokens
- LocalIsDarkTheme for M3-compatible dark mode detection

Phase 3: Sidebar redesign
- 240dp wide sidebar with icon + text labels (was 56dp icon-only)
- Animated collapse/expand with smooth width transition
- Avatar + username at top with account switcher
- Custom feeds section from FeedDefinitionRepository
- Active item cyan pill indicator with hover effects
- Collapse state persisted in Preferences
- Debounced fitColumnsToWidth to prevent animation thrash

Phase 4: Card refinement
- OutlinedCard with 1dp border replacing 1dp shadow elevation
- 16dp internal padding (was 12dp)
- Converted NoteCard, ReadsScreen, DraftsScreen, MyHighlightsScreen, UserProfileScreen

Phase 5: Column header restyling
- 48dp height (was 40dp) with surfaceContainer background
- 12dp horizontal padding (was 8dp)

Phase 6: Polish
- HoverModifiers.kt — shared hover highlight using onPointerEvent + drawBehind
- ShimmerPlaceholder.kt — skeleton loading animation in commons/commonMain

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 07:02:49 +03:00
Claude f8ff9049d9 fix(onchain): highlight bolt for own onchain zaps + own pending in counter
Two parallel gaps to the cashu work, surfaced once the cashu side
was wired correctly:

1. The orange bolt highlight on the reaction row never lit up for
   onchain zaps. Note.isZappedBy checked LN zaps, NWC payments,
   and (since Phase 1) nutzaps — but never onchainZaps. And the
   fast-path gate in ObserveZapIconState shared the same blind
   spot. Add isOnchainZappedBy parallel to isNutzappedBy (same
   shape: any onchainZaps entry whose source.author matches the
   user and whose source event is newer than afterTimeInSeconds),
   and extend the gate with onchainZaps?.isNotEmpty().

2. The reaction-row counter included CONFIRMED onchain amounts
   via updateZapTotal (verifiedSats only, per NIP-BC) but not
   the signed-in user's OWN pending/unverified outgoing zaps.
   That created a UX mismatch: the gallery shows the user's own
   UNVERIFIED entry with its claimed sat amount immediately
   (the user knows what they sent), but the counter stays at 0
   until the chain catches up. Add
   Note.extraOwnPendingOnchainSats(loggedInPubKey) that sums
   claimedSats from non-CONFIRMED onchainZaps whose source.author
   matches the logged-in pubkey, and add it on top of zapsAmount
   in both AccountViewModel.calculateZapAmount paths and
   ObserveZapAmountText's no-zapPayments fast path. Other senders'
   non-confirmed entries still contribute 0, preserving the
   anti-spoof posture for incoming zaps.
2026-05-28 23:06:10 +00:00
Claude aeadbbd276 Make TimeAgoFormatter + CalendarTimeFormat thread-safe
The module-level mutable SimpleDateFormat formatters in these files
were read concurrently — UI composition on the main thread, and
LocalCache.justVerify calling dateFormatter() from background event-
verification coroutines for failed-signature log lines. SimpleDateFormat
is not thread-safe (mutable internal Calendar), and updateFormattersIfNeeded
reassigned the field mid-format. Race produced corrupted timestamp
strings and occasionally NumberFormatException inside format().

Replace the shared-var pattern with a small LocaleAwareFormatter that
wraps a ThreadLocal<Pair<Locale, SimpleDateFormat>>. Each thread caches
its own instance and rebuilds lazily when Locale.getDefault() changes —
no locks, no contention, same allocation profile after warm-up.

Apply the same pattern to CalendarTimeFormat for consistency; today its
callers are all main-thread but the structure was identical.
2026-05-28 23:00:03 +00:00
Claude c679a870ab Respect Android system date/time format preferences
Hardcoded date/time patterns ignored the user's Locale (date order:
dd/mm/yyyy vs mm/dd/yyyy vs yyyy-mm-dd) and the system 12/24-hour
override. Replace them with locale-aware formatters that resolve order
from the active Locale via DateFormat.getBestDateTimePattern() and pick
the time-of-day pattern via DateFormat.is24HourFormat(context).

- TimeAgoFormatter (amethyst + commons): build SimpleDateFormat from
  Unicode LDML skeletons (yMMMd / MMMd / yMMM) so "May 28, 2026" in
  en-US becomes "28 May 2026" in en-GB, "28.05.2026" in de-DE, etc.
- CalendarTimeFormat: same skeleton approach for date pieces; time
  uses DateFormat.getTimeFormat(context) so a 24-hour Android user
  sees 14:32 even on a 12-hour locale.
- New LocalizedDateTimeFormat helper with formatMonthDayTime,
  formatMediumDate, formatMediumDateTime — used by wallet, vanish,
  attestation, namecoin, eventsync screens to replace inline
  SimpleDateFormat("MMM d, HH:mm") / ("MMM dd, yyyy  hh:mm a") etc.
- Material3 TimePicker callers (calendar/nest/poll/zap-poll/expiration
  date pickers + vanish request) now pass is24Hour from the system
  setting instead of hardcoding false.
- Desktop article/reads/highlights screens use
  java.text.DateFormat.getDateInstance(MEDIUM, locale).
- Drop dead formattedDateTime() in RelayCompose (was unused).

Intentionally left alone: notification feed bucket keys ("yyyy-MM-dd"
used as Map keys), TakePicture file naming (Locale.US), iCalendar
RFC 5545 stamps, NIP-52 ISO date storage, internal logging, and
ThreadLevelCalculator sort keys — none are user-facing.
2026-05-28 22:22:59 +00:00
Claude 12ed86627c feat(nutzap): fold nutzaps into reaction-row zap counter + icon highlight
Phase 0 (small fix): sendNutzap was async-launched with no success
callback, so after tapping the teal cashu chip in the zap picker
the popup vanished and the user saw no feedback for the 1-2 seconds
it took the swap + publish to complete. Add a "Cashu zap sent —
Sent N sat(s) via cashu" toast on success, matching the lightning
zap's progress feedback in spirit.

Phase 1 (foundation): NIP-61 nutzaps attach to their target note
the same way LN zaps and onchain zaps do, contributing to the
reaction-row total and the "you-already-zapped" icon highlight
without any UI-layer change.

Pieces:

- NutzapEvent.claimedSatsTotal() in quartz parses the sender-
  claimed sat sum from the proof tags once, leniently (a single
  malformed proof contributes 0 rather than throwing). The
  recipient wallet still verifies proofs against the mint at redeem
  time; this is the trusted-claim total for display.

- Note.nutzaps: Map<HexKey, NutzapEntry> on the canonical commons
  Note, parallel to onchainZaps. NutzapEntry carries the source
  kind:9321 note (sender = source.author) and the pre-parsed
  claimedSats. Volatile because writes happen on applicationIOScope
  and reads happen on the Compose main thread.

- updateZapTotal() now sums nutzap claimedSats into zapsAmount, so
  the existing ObserveZapAmountText composable in ReactionsRow
  picks up cashu without code change.

- hasZapped() and the suspend isZappedBy() extended to detect
  nutzaps from a given user. ReactionsRow's calculateIfNoteWasZap-
  pedByAccount path therefore highlights the bolt orange for cashu
  zaps the same way it does for lightning.

- LocalCache previously routed NutzapEvent through
  consumeRegularEvent, which would add it as a *reply* to the
  e-tagged note via computeReplyTo. computeReplyTo gains a
  NutzapEvent case returning the linked event ids, and a dedicated
  consume(NutzapEvent) function attaches via addNutzap instead of
  addReply.

The "list" merge across LN + cashu + onchain that the user floated
is deferred — three separate collections with different shapes
(zap pair, onchain entry, nutzap entry) are kept; only the
aggregates and queries are unified. That's enough for the
reaction-row UX and avoids touching every iteration site at the
call layer.

Coming next: notifications (NotificationFeedFilter + a cashu-icon
variant of ZapUserSetCard) and the dedicated cashu row in
ReactionDetailGallery modeled on OnchainZapGallery.
2026-05-28 22:20:25 +00:00
Claude df0bb2641a chore(commons): use Headphones + Podcasts glyphs for the podcast tabs
Swaps PlayCircle / AudioFile (generic) for the canonical Material Symbols
podcast iconography — `headphones` (U+F01F) on the Episodes feed and
`podcasts` (U+F048, the mic + signal-waves glyph) on the Shows feed.
Both codepoints added to MaterialSymbols.kt and the subset font
regenerated via tools/material-symbols-subset/subset.sh.
2026-05-28 20:56:00 +00:00
Claude a19a025274 Revert "fix(richtext): don't truncate single-atom content mid-token"
This reverts commit 3dbf247108.
2026-05-28 20:44:14 +00:00
Claude 3dbf247108 fix(richtext): don't truncate single-atom content mid-token
A cashuB token pasted into a DM didn't render the redeem card —
the user saw the raw base64 + a useless "Show more" button.

Root cause was in ExpandableTextCutOffCalculator. The user's
token was ~480 chars with no whitespace anywhere. The calculator
saw `min == content.length > TOO_FAR_SEARCH_THE_OTHER_WAY (450)`,
fell into the backward-search branch, found no space or newline
in the first SHORT_TEXT_LENGTH (350) chars either, and returned
350 — slicing the token mid-base64.

The truncated string still started with "cashuB", so the parser
matched a CashuSegment, but CashuPreview's base64 decode failed
on the corrupt body and it fell back to rendering the raw text.

Fix: when there's no whitespace boundary anywhere in the first
SHORT_TEXT_LENGTH chars during backward search, return
content.length — the entire content is one indivisible atom
(cashuA/cashuB, base64 data: URI, lnbc, single huge URL), so
cutting it can only corrupt the segment.

The pre-existing testImage was locking in the same bug for a
~11k-char data: URI (truncated to 350 → broken image segment);
updated it to assert image.length and added two new regression
tests around the user's exact cashuB token and a "preamble +
long token" case where the cut should land cleanly at the
boundary before the token. Also added a CashuTokenParserTest
covering the parser side (which was already correct) so any
future change that breaks cashuB detection is caught.
2026-05-28 20:27:53 +00:00
Claude d28458553a Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-amethyst-sdOWe 2026-05-27 20:02:35 +00:00
m 67edb32fa7 refactor(namecoin): consolidate NamecoinSettings into commons
Two NamecoinSettings classes had drifted:

- commons (used by Desktop): only enabled + customServers
- amethyst service.namecoin (used by Android): full schema with backend,
  namecoinCoreRpc, fallbackToCustomElectrumx, fallbackToDefaultElectrumx

This left Desktop unable to persist any of the Namecoin Core RPC or
fallback-policy state introduced in the Android settings UI. Promote
the rich Android version into commons as the single source of truth
and delete the Android duplicate.

- Move the rich schema (backend, namecoinCoreRpc, fallback toggles,
  hasUsableCoreRpc, toFallbackPolicy) into the commons NamecoinSettings.
- Delete amethyst/service/namecoin/NamecoinSettings.kt and its test.
- Repoint the two Android imports (NamecoinSharedPreferences,
  NamecoinSettingsSection) at the commons class. No behaviour change on
  Android.
- Fold the Android-only backend/RPC/fallback test cases into the commons
  NamecoinSettingsTest so the shared schema stays covered.

Desktop persistence (DesktopNamecoinPreferences) still only reads/writes
enabled + customServers; the extra commons fields fall back to defaults
on the existing Desktop store. Wiring those new fields into Desktop is
the next change.
2026-05-28 05:06:44 +10:00
Claude 1190abb30d refactor(user): eager-init pinned addressable notes
The three pinned per-user replaceable notes (NIP-65 / DM relays /
nutzap info) were lazy fields. Lazy delegation here adds a synchronized
read on every access for no gain — User is constructed via
LocalCache.getOrCreate, and the pinned notes are read on essentially
every interaction with the user. Resolving them at construction also
lets us drop the stored UserContext reference.
2026-05-27 16:19:37 +00:00
Claude 8fa636bbe8 refactor(model): lazy-pinned addressable notes on User via UserContext
Pins each per-user replaceable note to the User's lifetime so weak-ref
eviction from LocalCache.addressables can't lose them — same fix the
NIP-65 / DM relay list notes already had, generalised so adding new
pinned kinds is a one-liner.

Background: LocalCache.addressables is a LargeSoftCache<Address,
AddressableNote> backed by WeakReference. Without a strong reference
somewhere, an addressable note shell (and any event loaded into it) can
be cleared on any GC cycle even though it was successfully delivered.
The User constructor already held nip65RelayListNote / dmRelayListNote
fields exactly to defeat this for kinds 10002 and 10050. kind:10019
(NutzapInfoEvent) had no such pin, so the zap picker's "does this user
accept nutzaps?" check would silently return null for an evicted note —
the chip never showed even when the recipient had actually published.

This refactor:
1. Adds `UserContext` — a one-method `fun interface` exposing
   `addressableNote(addr): Note`. User holds it for life; LocalCache
   implements it via a single instance bound to ::getOrCreateAddressableNoteInternal.
2. Converts the three per-user pinned notes (nip65 / dm / nutzapInfo)
   to `by lazy` fields backed by the context. Each is resolved the
   first time it's read and then held by the User's strong reference
   until the User itself is collected. `by lazy`'s default SYNCHRONIZED
   mode handles concurrent reads from the zap picker + wallet state.
3. Adds typed accessors on User: nutzapInfo(), acceptsNutzaps(),
   nutzapMints(), nutzapP2pkPubkey() — mirrors the existing
   authorRelayList() / dmInboxRelayList() shape.
4. CashuWalletState.peekNutzapTarget now reads via
   `cache.getOrCreateUser(recipientPubKey).nutzapInfo()` instead of
   touching the cache's addressable map directly.

Tradeoffs vs the eager-constructor approach:
- No upfront allocation for kinds the screen never reads.
- Adding a new pinned kind (mute list, blocked relays, bookmark list)
  is one `by lazy { context.addressableNote(...) }` line in User —
  no constructor-signature churn across call sites.
- User now depends on a narrow `UserContext` interface; test fakes are
  a one-liner: `User(hex) { addr -> Note(addr.toValue()) }`.

Migration:
- Single User constructor call site (LocalCache.getOrCreateUser) updated.
- Two existing test fakes (NoteOnchainZapTest, SearchResultSorterTest)
  switched to the SAM-lambda form.
- No external behaviour change — the public `nip65RelayListNote` /
  `dmRelayListNote` fields keep the same names and types, so the few
  consumers (RelayFeedViewModel, ChatNewMessageViewModel) need no edits.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:43 +00:00
Claude 94121c71c9 feat(cashu): mint URL directory + autocomplete
Adds a cache-backed Cashu mint directory sibling to LocalCache.relayHints
that aggregates mint URLs from every relevant event the cache sees, and
wires it into the AddCashuWallet mint-URL text field as inline
autocomplete so users don't have to remember mint URLs.

What feeds the directory:
- NutzapInfoEvent (kind:10019) — every nostr user with a Cashu wallet
  publishes their accepted mints there. A typical inbox of cached
  profiles seeds a useful starter directory automatically.
- MintRecommendationEvent (kind:38000) — explicit public vouches.
- CashuMintEvent (kind:38172) — formal mint announcements from the
  NIP-87 directory subscription.

How it's populated:
- LocalCache.updateMintIndex(event) is called from
  justConsumeAndUpdateIndexes alongside updateHintIndexes, so every new
  event with a mint URL adds to the index. wasNew gating prevents
  re-emissions from inflating popularity counters.
- LocalCache.ensureMintDirectoryBackfilled() does a one-shot scan of the
  existing notes + addressables maps. The autocomplete UI kicks this in
  a LaunchedEffect on screen open so suggestions are useful before the
  next relay round-trip.

Where it surfaces today:
- AddCashuWalletScreen — under the mint-URL OutlinedTextField, a
  MintSuggestionList card shows up to 6 cache-derived suggestions ranked
  by popularity desc + URL asc. Tapping a row fills the field (does not
  auto-add — users typically want to Verify first). Filters out URLs the
  user already added and exact matches of what they typed.

The MintPicker dropdown inside the Receive / Send dialogs is unchanged
— those only need to choose between mints the user already has in their
wallet, so no directory autocomplete applies there.

Tests: 8 unit tests cover normalisation (case-insensitive, trailing-slash
stripping, http(s) gating), popularity ranking, substring filtering,
limit enforcement, and malformed-URL handling.

URL normalisation: trimmed, lower-cased, trailing `/` stripped, scheme
must be http(s). Same URL with different casing or trailing slash
collapses to one entry so popularity counts correctly.

Implementation notes:
- MintDirectoryIndex lives in commons/jvmAndroid (uses ConcurrentHashMap;
  iOS doesn't ship Cashu wallet yet).
- Thread-safe; safe to read from any dispatcher.
- No persistence — purely in-memory, accumulates over the session.
- Entries are never removed: stale entries don't hurt (user always
  verifies before adding), and tracking which event added which URL
  would add bookkeeping without UX benefit.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:42 +00:00
Claude 9d0224ba5b feat(cashu): Wallet Settings screen with retractable mint recommendations
The top-bar pencil on the Cashu Wallet screen becomes a gear that opens a
new Settings hub instead of jumping straight to the edit form. The hub
hosts:

- "Edit wallet details" → routes to the existing AddCashuWallet form in
  edit mode (mints + nutzap key).
- "My mint recommendations" → live list of NIP-87 kind:38000 events this
  account has published, each with a NIP-09 retract button. Retraction
  fires DeletionEvent with both `e` and (when a d-tag is present) `a`
  tags so compliant relays drop all versions of the parameterized-
  replaceable recommendation.

The wallet's existing CashuWalletFilterAssembler now pulls
MintRecommendationEvent.KIND alongside the other NIP-60 / NIP-61 kinds,
so the list populates without an extra subscription. CashuWalletState
indexes own recommendations into a new `ownRecommendations` StateFlow
(keyed by d-tag, falling back to event id for malformed events) and
keeps it in sync via the existing live cache + delete observers.

Future settings (auto-recommend toggle, nutzap relay overrides,
export/backup) belong here — consolidating wallet-shaped knobs in one
place avoids re-cluttering the main wallet screen.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:42 +00:00
Claude ba46f31359 feat(cashu): NIP-87 mint discovery + recommendations
Adds end-to-end NIP-87 support so users can pick mints from the
network instead of having to know URLs upfront, and can publicly
endorse mints they use.

Discovery (commons + amethyst)
  * commons/.../CashuMintDirectoryFilterAssembler — subscribes to
    kind:38172 cashu mint announcements and kind:38000 cashu-scoped
    recommendations (#k=["38172"]) on a configurable relay set.
    Fedimint announcements (38173) are intentionally excluded — this
    feeds the Cashu mint picker only.
  * RelaySubscriptionsCoordinator.cashuMintDirectory — singleton
    assembler reachable as Amethyst.instance.sources.cashuMintDirectory.

Indexing state (amethyst/model)
  * CashuMintDirectoryState — account-scoped index of announcements +
    recommendations. Reactive: backfills from LocalCache.notes on
    first observer and listens to LocalCache.live.newEventBundles for
    incremental updates. The relay subscription only runs while at
    least one picker is on screen (ref-counted open()/close()).
  * Ranking: follows-recommendations DESC, then total recommendations
    DESC, then URL ASC. Dedup'd by (recommender, mint URL) so a
    single recommender can't inflate counts by re-posting.
  * CashuMintDirectoryEntry — display model with URL, latest
    announcement, total and follows-recommendation counts.

Publishing recommendations (CashuWalletOps)
  * recommendMint(mintUrl, dTag?, review) — publishes kind:38000 with
    both the `a`-tag (pointing at the mint's announcement by
    kind:pubkey:dTag) and a `u`-tag with the raw URL so older clients
    indexing by URL still pick it up.

UI integration
  * MintPickerSheet — ModalBottomSheet with search field + scrollable
    list. Each row shows the mint name (parsed from the announcement
    content) or URL, with badge chips for "from people you follow"
    and total recommendation counts. The "Add" button writes the URL
    back to the caller's mints list; already-added URLs show "Added"
    instead.
  * AddCashuWalletScreen gets a "Browse" button next to the Mints
    section header that opens the picker. Selected mints are still
    Verify-able via the existing ping; users can still paste manually
    if they want.
  * CashuWalletScreen's mint list gets a thumb-up icon button per
    mint that fires viewModel.recommendMint(url) — best-effort,
    silent failure (logged via Log.w("CashuWallet")).

Wiring
  * cashuMintDirectoryFilterAssembler factory plumbs through Account →
    AccountCacheState → AppModules. The mock test AccountViewModel
    constructions in AccountViewModel.kt are updated to pass a fresh
    assembler.

playDebug + fdroidDebug compile clean. 24/24 jvm tests still passing.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:41 +00:00
Claude 5cd756cea2 refactor(cashu): lift wallet state to Account, react to live cache updates
Addresses the critical findings from the post-implementation audit:

A1. State holder lives on Account, not the ViewModel
  New CashuWalletState owns the wallet event, decrypted token contents,
  history, mint-quote, and inbound-nutzap indexes. It's constructed on
  Account and runs for the lifetime of the login session — so nutzaps
  arriving while the user is on Home/DMs/etc. get auto-redeemed without
  requiring the wallet screen to be open. ViewModel becomes a thin
  presenter that forwards flows + holds per-flow UI state (mint quote
  in progress, melt confirmation pending).

A2. Reactive observation via LocalCache.live.newEventBundles
  The state object backfills once from cache.notes at construction time,
  then receives incremental updates from the live new/deleted event
  bundles for any NIP-60/61 event authored by us (or addressed to us
  via #p for nutzaps). NIP-44 decryption results for kind:7375 events
  are cached by event-id, so the per-refresh re-decrypt is gone (D2).

A3. Mutex-guarded auto-redeem (no more duplicate /v1/swap races)
  redeemPendingNutzapsSerialized uses tryLock so a sweep already in
  flight short-circuits any new triggers; subsequent cache updates
  catch up via the next bundle.

A4. Mint-quote recovery on launch
  pendingQuotes flow surfaces unfulfilled kind:7374 events whose
  expiration hasn't passed and whose id isn't yet referenced with a
  "destroyed" marker in any kind:7376. ViewModel.resumeMintQuote()
  re-polls the mint for the original quote and rebuilds the flow.

B1. NutzapInfoEvent now carries the wallet's outbox relays so senders
  publish nutzaps where our assembler is actually listening.

B2. Subscription tracks the outboxRelaysFlow — when the relay list
  changes, the assembler subscription is rebuilt with the new set.

B5. New MintProtocolException distinguishes "HTTP fine, protocol said
  no" (e.g. melt state != PAID) from "HTTP error". Both surface
  through describeMintError() (now top-level — C4).

B7. redeemNutzap now pre-checks the P2PK secret's pubkey matches our
  wallet pubkey before signing — saves a wasted mint round-trip when
  the lock targets someone else.

B8. Melt is a two-phase flow: startMelt() returns a Quoted state with
  amount + fee_reserve so the UI confirms before paying; confirmMelt()
  actually spends. No more silent fee acceptance.

C1. MintHttpClient + CashuMintOperations cached per mint URL via a
  ConcurrentHashMap.

C3. AddCashuWalletScreen has a "Verify" button that pings /v1/info
  before adding, with inline success / failure feedback.

C7. Inline JsonObject FQN in P2PK.kt replaced with proper import.

C8. Dead .also { _ -> secretJson } removed from redeemNutzap.

D1. runCatching {}.getOrNull() callsites in the state holder now log
  via Log.w("CashuWallet") so silent failures surface in logcat.

D5. CashuWalletQueryState made @Immutable + data class for Compose
  stability hygiene.

Touched files: Account.kt (state field + constructor params),
AccountCacheState.kt + AppModules.kt (wire the assembler factory +
okHttpClientForMoney through), CashuWalletOps.kt (decouples from
Account, takes signer + publish callback), CashuWalletState.kt (new),
CashuWalletViewModel.kt (presenter rewrite), CashuWalletScreen.kt
(two-phase melt UI), AddCashuWalletScreen.kt (Verify button),
strings.xml (new keys).

All 20 NIP-60 jvm tests still pass; playDebug + fdroidDebug compile
clean.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:40 +00:00
Claude 16401e536a feat(cashu): full NIP-60 wallet + NIP-61 nutzap receive
Builds out the Cashu wallet beyond the scaffold: a complete mint
protocol layer, the four user-facing wallet operations (mint, melt,
send-as-token, redeem), and auto-redemption of inbound NIP-61
nutzaps. Wires the relay subscription so the wallet state syncs
across devices.

quartz/ — mint protocol layer (commonMain + jvmAndroid)
  * nip60Cashu/mintApi/MintApiDtos.kt — Kotlinx Serialization DTOs
    for NUT-00..06 (info, keys, mint/quote/bolt11, mint/bolt11,
    swap, melt/quote/bolt11, melt/bolt11, checkstate). ProofDto
    carries the optional NUT-11 witness.
  * nip60Cashu/mintApi/MintHttpClient.kt — OkHttp + kotlinx-json
    client bound to a single mint URL; surfaces MintHttpException
    with the mint's detail string preserved for the UI.
  * nip60Cashu/mintApi/CashuMintOperations.kt — combines BDHKE +
    HTTP + amount splitting. Exposes requestMintQuote / mintProofs
    / swap / requestMeltQuote / meltProofs / redeemNutzap. Power-
    of-2 amount split per NUT-00.
  * nip60Cashu/mintApi/AmountSplit.kt — extracted into commonMain
    for testability.
  * nip60Cashu/p2pk/P2PK.kt — NUT-11 locked-secret format and
    BIP-340 Schnorr witness signing.
  * CashuProof gains an optional witness field.

amethyst/ — wallet ops + UI
  * model/nip60Cashu/CashuWalletOps.kt — Nostr publishing layer
    over CashuMintOperations:
      - publishWalletEvents (kind 17375 + kind 10019 together)
      - startMintFromLightning / checkMintQuote /
        completeMintFromLightning (kind 7374 lifecycle + 7375 +
        7376 + NIP-09 deletion of the quote)
      - meltToLightning (pre-swap if needed, melt, change rollover,
        delete sources, history)
      - sendAsToken (swap to exact split, V4Encoder for cashuB,
        rollover, history)
      - redeemToken (inbound cashuA/B via swap)
      - redeemNutzap (NIP-61 P2PK unlock + swap, history with
        unencrypted "redeemed" marker per spec)
  * service/cashu/v4/V4Encoder.kt — inverse of the existing
    V4Parser; encodes proofs to cashuB strings for send.
  * ui/screen/loggedIn/wallet/CashuWalletScreen.kt — adds four
    action buttons (Receive / Send LN / Send Token / Redeem) with
    AlertDialog-based flows that poll the mint quote, paste/copy
    from clipboard, and surface mint errors.
  * ui/screen/loggedIn/wallet/CashuWalletViewModel.kt — new mint
    / melt / send-token / redeem state machines, subscribes via
    CashuWalletFilterAssembler on init (auto-syncs the wallet
    across devices), observes the wallet note's flow for reactive
    refresh, and auto-redeems any inbound kind 9321 nutzap that
    isn't already marked redeemed in our kind 7376 history.

relay subscription
  * commons/.../CashuWalletFilterAssembler.kt refactored into the
    standard ComposeSubscriptionManager + SingleSubEoseManager
    pair (matches the NWC pattern). Now driven by subscribe(query)
    / unsubscribe(query) calls from the ViewModel.
  * RelaySubscriptionsCoordinator.cashuWallet exposes a singleton
    assembler reachable as Amethyst.instance.sources.cashuWallet.

Tests (jvmTest)
  * BdhkeTest — 7/7
  * AmountSplitTest — 7/7 (NUT-00 vectors + sum invariants)
  * P2PKTest — 6/6 (secret round-trip, witness verifies under
    BIP-340, compressed + x-only acceptance)

Total: 20 new NIP-60 jvm tests, all passing. Both playDebug and
fdroidDebug compile clean.

Deferred (clearly bounded follow-ups):
  * Sending nutzaps (kind 9321) from the zap picker UI — requires
    integrating with the existing LN zap chooser surface. The
    underlying P2PK locking primitives are in place.
  * Recovering an interrupted kind 7374 mint quote on next launch
    — current flow keeps polling while the dialog stays open.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:40 +00:00