Commit Graph
14544 Commits
Author SHA1 Message Date
Claude 37b07057b2 refactor(quartz): enforce session-level limits through policy hooks
Previously max_message_length and max_subscriptions were hard-coded in
RelaySession behind a parallel `limits` param, while the per-command limits
went through LimitsPolicy — two mechanisms, and a custom policy couldn't
influence the session-level ones.

Unify them: add two default-noop hooks to IRelayPolicy —
acceptMessage(raw) (pre-parse) and acceptSubscription(subId, openCount) —
chained through PolicyStack so they compose across multiple policies.
LimitsPolicy now implements all limit checks; RelaySession just invokes the
hooks and no longer takes a `limits` param. Servers compose LimitsPolicy
whenever `limits` is set and keep `limits` only to advertise via NIP-11.

Behaviour is unchanged (oversized -> NOTICE invalid:, sub cap -> CLOSED
rate-limited:); the enforcement now lives in the policy layer. Adds direct
hook unit tests; existing end-to-end limit tests still pass.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 18:58:20 +00:00
Claude 70dcaa1b10 feat(quartz): relay limits as single source of truth + NIP-11 serving
A relay author no longer hand-rolls limit policies or wires NIP-11 twice.

- RelayLimits: one config that is both enforced and advertised. Pass it to
  NostrServer/ReqResponderServer and every limit is applied; toNip11Limitation()
  renders the same numbers into the NIP-11 limitation block so they can't drift.
- LimitsPolicy: per-command enforcement (max_content_length, max_event_tags,
  created_at bounds reject EVENT; max_filters / max_subid_length reject
  REQ/COUNT; max_limit clamps, default_limit fills) with invalid: prefixes.
  Servers prepend it automatically when limits declares command caps.
- RelaySession: session-level caps that a policy can't see — max_message_length
  (NOTICE before parse) and max_subscriptions (rate-limited: CLOSED on new sub).
- NIP-11 serving: Nip11RelayInformation.toJson() + CONTENT_TYPE
  (application/nostr+json); the existing model was parse-only.
- Tests for the policy, the session-level caps end-to-end, and the
  limits->NIP-11 round trip; RELAY.md Limits + Serving NIP-11 sections.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 18:41:06 +00:00
Claude 952da685d8 feat(quartz): NIP-45 approximate COUNT — HyperLogLog construction + wire support
The HLL aggregation side (estimate/merge/encode) existed but the construction
side did not, and the Jackson wire (de)serializer silently dropped the `hll`
field — so a JVM/Android relay could not actually answer COUNT with HLL.

- HyperLogLog.addPubKey(): the NIP-45 construction (register index = pubkey
  byte at the filter offset; value = leading-zero-bits from offset+1, +1),
  KMP-safe. Plus HyperLogLog.builderFor(filter) and an HllBuilder that streams
  event pubkeys into registers and yields an approximate CountResult.
- Plumb CountResult through the count path: SessionBackend.countResult /
  ReqResponder.countResult (default = exact count(); override for approximate/
  hll); RelaySession sends the returned CountResult.
- Fix CountResultSerializer/Deserializer (jvmAndroid) to write/read the `hll`
  hex field, matching the kotlinx serializer the native targets already use.
- Tests for construction (index/value/merge-idempotence) and the wire path;
  RELAY.md Approximate COUNT section.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 18:29:01 +00:00
Claude 319ffb729a feat(quartz): relay connection observability + stable connection ids
Gives relay operators metrics/logging hooks without patching the engine, and
removes the hashCode()-keyed connection registry the audit flagged.

- RelaySession gains a stable, process-unique `id` (monotonic counter).
- RelayConnectionListener (onConnect/onDisconnect, no-op default) is accepted
  by NostrServer and ReqResponderServer; both now key their connection
  registry by `id` instead of hashCode() (no more identity-collision hole).
- Both servers expose a live `activeConnections` gauge. Teardown accounting is
  idempotent (double close counted once) and onDisconnect fires for any
  connections still open at server close.
- Tests + RELAY.md Observability section.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 18:21:05 +00:00
Claude bcb4b2b964 fix(quartz): roll back authentication when the post-auth hook rejects
Audit finding: FullAuthPolicy.accept(AuthCmd) added the pubkey to the
authenticated set before onAuthenticated ran, so a bridge that threw from
onAuthenticated (its whole point — reject when e.g. a JWT exchange fails)
produced an OK false while the connection stayed authenticated server-side.
Subsequent REQ/EVENT/COUNT were then allowed despite the failed login — an
auth bypass.

- Add IRelayPolicy.onAuthenticationFailed(pubKey) (default no-op), forwarded
  by PolicyStack and overridden by FullAuthPolicy to drop the pubkey.
- RelaySession.handleAuth calls it when onAuthenticated throws, restoring the
  invariant that a client treated as authenticated is exactly one that got
  OK true. The rollback is itself guarded so a misbehaving policy can't also
  swallow the failing OK.
- Tests: failed-hook now asserts the connection is NOT authenticated and that
  a follow-up REQ is rejected with auth-required.
- RELAY.md: note that throwing from onAuthenticated rolls auth back.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 17:45:52 +00:00
Claude 68e49a97ba feat(quartz): Flow<Event> REQ-responder SPI + storage-free dispatch engine
Lets non-storage relays (search, redirector, computed/projected data) answer
REQs without implementing the heavy IEventStore or hand-writing the
readFrame -> parse -> policy -> EVENT/EOSE loop.

- ReqResponder: the public Flow<Event> SPI — respond(filters): Flow<Event>
  (+ a count() default). EOSE is sent when the flow completes.
- SessionBackend: the seam RelaySession now depends on (query/count/submit/
  negentropy-snapshot). submit + snapshot default to reject / empty so a
  responder only implements the read path. LiveEventStore implements it
  (storage path unchanged); ReqResponderBackend adapts a ReqResponder.
- ReqResponderServer: storage-free dispatch engine mirroring NostrServer's
  connect/serve/close, reusing RelaySession for the full wire protocol.
- RelaySession now frames backend failures as CLOSED error: <msg> (REQ) and
  count failures likewise, instead of dropping the coroutine — useful for
  responders doing network I/O.
- RELAY.md: Non-Storage Relays section + engine/source-map updates.

Storage path (NostrServer + IEventStore, live tail, negentropy) is unchanged;
existing server/auth/negentropy tests pass alongside the new responder tests.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 17:31:53 +00:00
Claude 69aea29954 feat(quartz): server-side relay ergonomics — NIP-50 parser, suspend auth hook, wire helpers
Addresses the self-contained, low-risk items from the relay-ergonomics
request:

- NIP-50: add SearchQuery to parse Filter.search into free-text terms and
  the typed key:value extensions (domain/language/sentiment/nsfw/include),
  preserving unknown extensions and offering a canonical toSearchString().
- NIP-42: add a suspend IRelayPolicy.onAuthenticated(pubKey, event) hook
  (chained through PolicyStack) so external-auth bridges (e.g. JWT exchange)
  can live inside FullAuthPolicy instead of leaking into transport code.
  RelaySession invokes it after the AUTH passes; a throw becomes OK false.
- Ergonomics: Command.fromJson/toJson and Message.fromJson/toJson mirroring
  Event, plus MachineReadablePrefix + OkMessage/ClosedMessage factories for
  standardized OK/CLOSED reason prefixes.
- Docs: RELAY.md sections for the external-auth bridge, NIP-50 search, and
  the wire helpers.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 17:15:13 +00:00
Vitor PamplonaandGitHub 9423463f17 Merge pull request #3129 from vitorpamplona/claude/sleepy-sagan-Q2p3t
Register CommunityRulesEvent in EventFactory for kind 34551
2026-06-03 09:35:28 -04:00
Claude 3a43c4f7bb fix: register CommunityRulesEvent in EventFactory
Kind 34551 (CommunityRulesEvent) was missing from EventFactory.create, so
signing a community-rules template produced a generic Event. Returning it
as CommunityRulesEvent in Account.sendCommunityRules threw a
ClassCastException when publishing community rules.

Register the kind in the factory and add a regression test.
2026-06-03 13:30:52 +00:00
David KasparandGitHub 479c694e6e Merge pull request #3128 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-03 14:35:31 +02:00
Crowdin Bot 6f645511f0 New Crowdin translations by GitHub Action 2026-06-03 12:02:06 +00:00
Vitor PamplonaandGitHub 67d14fcc03 Merge pull request #3125 from nrobi144/fix/desktop-log-noise
fix: address root causes of 6 runtime log noise issues
2026-06-03 07:59:09 -04:00
Vitor PamplonaandGitHub 74af87ae37 Merge pull request #3127 from davotoula/feat/settings-search
Searchable, data-driven settings screen
2026-06-03 07:58:50 -04:00
davotoulaandClaude Opus 4.8 72d538bb62 refactor(settings): address review — non-translatable keywords, symEntry helper, legal keywords
- Mark all *_search_keywords translatable="false" (English concept/protocol
  index; stops Crowdin translating protocol terms and breaking locale search) [#1]
- Collapse ~23 symbol+nav rows via a local symEntry() helper [#3]
- Reword keywordsRes KDoc to match the actual word-prefix tokenization [#6]
- Add search keywords to the Legal rows (privacy_policy, child_safety) [#7]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:08:28 +02:00
davotoula d75e80bea6 feat(settings): curated search keywords for all rows + word-prefix search matching
Add keyword blobs so settings resolve by concept/protocol name, not just
title — e.g. "blossom" -> Media Servers, "audio rooms" -> Nests Servers,
"negentropy" -> Event Sync, "nsec" -> Backup Keys. NIP numbers omitted by
preference.
2026-06-03 09:45:20 +02:00
David KasparandGitHub fc14ad6bfc Merge pull request #3126 from nrobi144/feat/desktop-new-posts-chip
feat(desktop): home-feed scroll polish + sidebar tooltips
2026-06-03 09:43:05 +02:00
davotoula d8e4ada781 Code reviewP
- Collapse filterSettings' two identical lookup lambdas into one stringLookup
- Rebuild via SettingsCategory.copy() so new fields aren't silently dropped
- Memoize buildSettingsCatalog with remember(hasPrivateKey, nav, uriHandler);
  onResetMarmot reads isResettingMarmot via rememberUpdatedState to avoid a
  stale-capture, eliminating ~60 allocations per keystroke
2026-06-03 09:21:24 +02:00
davotoula 4446b12972 feat(settings): add settings catalog data model + filterSettings
feat(settings): add search box that filters settings rows by title + keywords
refactor(settings): expose legal section as legalSettingsCategory factory
feat(settings): add search placeholder, empty-state, and keyword strings
test(settings): cover filterSettings (blank/title/keyword/category/empty/danger)
refactor(settings): match category title in search; use data classes
2026-06-03 09:20:54 +02:00
nrobi144 599a16193a fix(desktop): collapsed sidebar — tighter ripple + hover tooltip
Two related polish fixes on the collapsed sidebar:

1. The hover/active highlight on each nav item used to span the full
   sidebar width (minus 8dp outer padding), producing ~12dp of empty
   highlight either side of the 24dp icon. Now the highlight clips to
   a 40dp square centered on the icon (24dp icon + 8dp padding on each
   side), so the ripple sits tight against the glyph.

2. When the sidebar is collapsed, the label was already supplied as
   `contentDescription` for screen readers but had no visual
   affordance. Added a `TooltipArea` that surfaces the label on hover
   (Surface + inverseSurface tonal style, matching the existing
   TorStatusIndicator tooltip pattern), so mouse users can also see
   what each icon means without expanding the sidebar.

Applied to both `SidebarNavItem` and `SidebarFeedItem` since both
suffer the same issue. Expanded behaviour is unchanged.
2026-06-03 09:46:15 +03:00
nrobi144 e5b210d4e1 fix(desktop): make first-pinned-feed default actually take effect
Two bugs that together caused HomeFeed to always open on Following:

1. FeedScreen was reading feedRepo.pinnedFeeds.value as the source of
   truth for the first pinned feed. That's a stateIn-derived flow with
   initial value persistentListOf(); the underlying _feeds StateFlow
   IS loaded synchronously by FeedDefinitionRepository on construction,
   but the derived pinnedFeeds doesn't reflect it until the first flow
   emission propagates — which is too late for `remember` to see.
   Fixed by reading feedRepo.feeds.value directly and filtering /
   sorting by pinOrder ourselves.

2. DeckColumnContainer was passing initialFeedMode = FeedMode.FOLLOWING
   when rendering DeckColumnType.HomeFeed, which overrode FeedScreen's
   first-pinned logic entirely. Removed the hardcode so the deck's
   home column inherits FeedScreen's default.

With both fixed, a user who has only Global pinned now opens to Global
on launch instead of Following.
2026-06-03 09:36:18 +03:00
nrobi144 99af0f75e1 fix(desktop): default home tab to first pinned feed, not last-saved mode
If the user has pinned only Global (or only a custom feed), the app
should open to that on launch instead of showing Following just
because DesktopPreferences.feedMode happened to be saved as
Following. The "pinned feeds" list is the user's stated ordering;
the first item should drive the initial tab.

Resolution order (most specific wins):
  1. explicit customFeedSource/customFeedId from the caller
  2. explicit initialFeedMode from the caller
  3. first pinned feed in feedRepo.pinnedFeeds (NEW)
  4. DesktopPreferences.feedMode (last-saved, previous default)

For a pinned Filter feed, this also seeds activeFeedId and
activeFeedSource so the feed mounts in CUSTOM mode with the right
source.
2026-06-03 09:31:02 +03:00
nrobi144 37662eea45 fix(desktop): port StickToTopOnPrepend to commons and apply on home feed
Real root cause of the "stale feed on launch" perception bug: when
fresh events prepend to the desktop home feed, Compose's stable-key
diff (`items(loadedState.list, key = { it.idHex })`) preserves the
visual anchor on whatever item was already visible. The user's
previously-visible top item — once at index 0 — silently shifts to
index N as N new items are inserted above the viewport. From the
user's perspective the feed looks frozen on stale items even though
the underlying state HAS updated; switching screens unmounts
FeedScreen, recreates lazyListState at index 0, and on remount paints
from the now-current top.

Android already handles this with StickToTopOnPrepend
(amethyst/.../WatchScrollToTop.kt:133-152), but the helper lived in
the Android module and Desktop had no equivalent.

Changes:

- New commons/.../ui/feeds/StickToTopOnPrepend.kt with the same
  observer + snapshotFlow trick, ported to use plain `collectAsState`
  (replacing the Android-only `collectAsStateWithLifecycle` — the
  effect's lifecycle is already bound to composition via
  LaunchedEffect). Provides the same overloads:
    * StickToTopOnPrepend(LazyListState, firstItemKey)
    * StickToTopOnPrepend(LazyGridState, firstItemKey)
    * StickToTopOnPrepend(FeedContentState, LazyListState)
    * StickToTopOnPrepend(FeedContentState, LazyGridState)
- FeedScreen wires StickToTopOnPrepend(viewModel.feedState,
  homeFeedLazyListState) at the same scope as the hoisted lazy list
  state and the NewPostsChip.

Mutually exclusive with the NewPostsChip: the chip's visibility
predicate fires when isAtTop is false, the auto-snap fires when
isAtTop is true. Together they cover both cases:
  * user at top → events arrive → auto-snap shows them
  * user scrolled down → events arrive → chip announces them

The Android version in amethyst/.../WatchScrollToTop.kt is left in
place to avoid a wider refactor; it can be reduced to a thin delegate
in a follow-up.
2026-06-03 07:33:39 +03:00
nrobi144 44febcc77f feat(desktop): add Amethyst logo to Tor and account-loading splashes
Both loading splashes (the Tor-connect gate and the account-loading
screen between Tor active and LoginScreen) now show the Amethyst
icon tinted to the theme primary, anchored below the status text.

Layout pattern (status-forward, both splashes):
  spinner → status text → Amethyst logo (96.dp, primary tint)

Brief research summary backing the choice:
- Apple HIG argues against splash branding, but its model assumes
  near-instant launch — not applicable here where the Tor gate
  can block for seconds.
- Material Design 2's branded-launch-screen pattern endorses
  logo + brand color while a placeholder UI loads.
- The status-forward order keeps the dynamic info (what we're
  waiting on) leading and the brand as the anchor below — the
  right call when the wait is non-trivial.
2026-06-03 07:31:49 +03:00
nrobi144 38a191341f fix(desktop): bump new-posts chip top margin to 16dp
Tighter 8dp gap clipped visually too close to the search header card.
2026-06-03 07:31:34 +03:00
nrobi144 098a74ca53 feat(desktop): add "New posts" chip with slide-from-top animation
Fixes the perceptual "stale feed on launch" bug: on cold launch the
desktop feed paints with whatever local cache had (up to 7 days old)
before relays catch up. The live updateFeedWith() path already prepends
fresh events silently, but users had no signal that fresh content
arrived unless they were already at the top of the feed (auto-snap via
StickToTopOnPrepend).

This adds a Twitter/Mastodon-style floating pill chip that slides down
from above the search header when fresh events have prepended AND the
user is scrolled below position 0. Tapping it smooth-scrolls to top
and slides the chip back up off-screen. Scrolling to top manually
also dismisses it.

Implementation:

- NewPostsChip + rememberNewPostsChipState in commons/commonMain so any
  future feed surface (incl. Android, iOS) can adopt it. Desktop wires
  it today; Android continues with the existing auto-stick + bottom-nav
  dot pattern.
- Visibility predicate is pure-function and unit-tested (5 cases).
- Predicate mirrors the inverse of StickToTopOnPrepend's "at top" check
  so the two systems are mutually exclusive — auto-snap when at top,
  chip when not.
- Chip placement: floating Alignment.TopCenter inside FeedScreen's outer
  Box, offset by the animated headerSpacerHeight (60.dp normal,
  300.dp when search is expanded) so it tracks the header card.
- Hoisted lazyListState + headerSpacerHeight one level so the chip can
  share scroll state with the LazyColumn. Existing viewport-aware
  metadata loading is unchanged (same lazyListState reference).
- Animation: slideInVertically(tween(280, FastOutSlowInEasing)) + fadeIn
  for enter; slideOutVertically(tween(220, FastOutLinearInEasing)) +
  fadeOut for exit. Initial/target offset of -fullHeight-16 guarantees
  the chip is fully off-screen above its rest position.
- Per-column scope by construction: each FeedScreen instance has its
  own chip state (deck mode shows one chip per column).
- Resets cleanly on feed mode switch (Following ↔ Global ↔ Custom)
  because rememberNewPostsChipState is keyed on FeedContentState,
  which is recreated when viewModel = remember(feedMode, activeFeedId)
  recomposes.

Plan: docs/plans/2026-06-02-feat-new-posts-chip-desktop-feed-plan.md
2026-06-02 17:16:58 +03:00
Vitor PamplonaandGitHub a4aff84897 Merge pull request #3124 from nrobi144/feat/desktop-feed-ui-refresh
feat(desktop): Feed UI refresh — inline expansion, comments, related content
2026-06-02 08:02:50 -04:00
Vitor PamplonaandGitHub 3de0fdc4c5 Merge pull request #3122 from davotoula/feat/share-as-dm
Share content directly to a DM ("Send as DM" share target)
2026-06-02 08:00:35 -04:00
nrobi144andClaude Opus 4.7 aeb49c3cac fix(desktop): address PR review findings on feed UI refresh
5 issues from davotoula's review on PR #3124:

- #3 (protocol): inline reply emitted a minimal e/p tag set instead of
  NIP-10. Extract `commons/actions/ReplyActions.replyTo` wrapping
  `TextNoteEvent.build(replyingTo=)` (which already encodes root marker,
  reply marker, parent root-e-tag carry) + carry parent's p-tag chain via
  `notify(...)`. Replies to deep-thread notes now thread correctly in
  Damus/Primal/Coracle. Covered by `ReplyActionsTest`.

- #4 (architecture): reaction/follow/reply each inlined
  `localCache.consume + relayManager.broadcastToAll` in 5 sites with
  inconsistent ordering. Extract `desktopApp/cache/dispatch(...)` —
  canonical local-first order — and route all 5 sites through it.

- #1 (UX): related-content section scanned the cache once via
  `DisposableEffect(noteId)` and never refreshed. Switch to `produceState`
  collecting `DesktopLocalCache.eventStream.newEventBundles`; re-scan only
  when an arriving bundle contains a candidate (matching hashtag or
  author). `LargeCache.notes` is a ConcurrentSkipListMap (weakly consistent
  iterator) so the scan stays safe on the composition coroutine.

- #2 (UX): `DeckColumnContainer` re-requested focus on every
  `currentOverlay` change, stealing focus from sibling columns whenever
  any column mutated overlay state. Drop to `LaunchedEffect(Unit)` and
  wrap the column in `key(column.id)` in `DeckLayout` so the one-shot
  effect survives column reordering.

- #5 (consistency): zap totals bypassed the shared `ZapFormatter`. Wire
  `RelatedContentRow`, `CommentItem`, and `NoteActions` to
  `commons/util/ZapFormatter.{showAmount,toZapAmount}`; delete
  `formatZapAmount` and `formatSats` desktop-local helpers.
  `WalletColumnScreen.formatSats` intentionally kept — locale-aware full
  precision for wallet balance is by design.

Plan: docs/plans/2026-06-02-fix-desktop-feed-review-findings-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 13:45:00 +03:00
Róbert NagyandGitHub 1f81a7fb25 Merge branch 'main' into fix/desktop-log-noise 2026-06-02 10:51:59 +03:00
nrobi144andClaude Opus 4.6 2ca8eb31dc fix: address root causes of 6 runtime log noise issues
1. LocalRelayStore: use batchInsert() with per-row savepoints instead of
   manual transaction — UNIQUE constraint violations skip that row instead
   of failing the whole batch

2. Robohash empty hex: guard blank input in CachedRobohash.get() with a
   fallback all-zeros hex key instead of passing empty string to assembler

3. GiftWrapEvent decrypt: downgrade from WARN to DEBUG — expected when
   gift wraps from local relay cache aren't addressed to current user
   (subscription filter is correct, but hydration doesn't filter by p-tag)

4. Relay URL %20: decode percent-encoded spaces before rejection check in
   RelayUrlNormalizer.fix() — wss://relay.example.com/%20 now normalizes
   to wss://relay.example.com/ instead of being rejected

5. NIP19 Parser: downgrade from ERROR/WARN to DEBUG — malformed bech32
   from relay content is expected in the wild, catch+log is correct

6. VLC macOS: add --avcodec-hw=none (disables VideoToolbox that causes
   CVPN chroma failures) and --reset-plugins-cache (rebuilds stale cache
   on startup instead of logging hundreds of stale-cache errors)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:47:58 +03:00
Róbert NagyandGitHub 70636c0f9a Merge branch 'main' into feat/desktop-feed-ui-refresh 2026-06-02 10:01:10 +03:00
Vitor PamplonaandGitHub 063da53ffd Merge pull request #3123 from vitorpamplona/claude/relaxed-edison-N0F4J
Support ephemeral signers for anonymous post uploads
2026-06-01 18:57:22 -04:00
Claude e8a50bfa11 fix: use ephemeral signer for media uploads in anonymous posts
When composing an anonymous post (tap pfp to go anon on the short-note
or comment screens), media uploads still authorized against the Blossom /
NIP-96 server with the real account's signer. The server echoes that
pubkey back in the returned media URL (e.g. Blossom's `as=<pubkey>`),
linking the real identity to the supposedly anonymous post.

Thread an optional `forcedSigner` through the upload chain
(MultiOrchestrator -> UploadOrchestrator -> NIP-96/Blossom auth). Both
ShortNotePostViewModel and CommentPostViewModel now hold a single
ephemeral signer per compose session, reused for every photo/voice
upload and for the final anonymous broadcast, so the upload auth event
and the post share one throwaway key. signAnonymouslyAndBroadcast accepts
that signer so the media author matches the post author. Non-anonymous
callers are unaffected (forcedSigner defaults to null).

The signer is reset in cancel() so each new compose session gets a fresh
anonymous identity.
2026-06-01 22:33:42 +00:00
davotoulaandClaude Opus 4.8 ae271d0ba7 refactor(sonar): extract NOT_STARTED_MESSAGE constant in CashuWalletState
Replace the literal "CashuWalletState.start() not called" duplicated across
9 call sites (8 check guards + the publish default lambda) with a single
private companion constant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:34:18 +02:00
davotoula e6a512db42 Code review and testing fixes:
- fix(dm-share): kotlin-review fixes (alias dot-boundary match + transient feed doc)
- fix(dm-share): address code-review findings (intent consume, media helper, manifest sync)
- fix(dm-share): make the picker one-shot so backing out doesn't duplicate drafts
- fix(dm): avoid duplicate drafts on abort by rotating draft tag after the async save
2026-06-01 21:29:41 +02:00
davotoula 8d715e5730 feat(dm-share): add ShareToDM route and attachment param on Room route 2026-06-01 21:29:41 +02:00
Vitor PamplonaandGitHub d8fba6a342 Merge pull request #3121 from vitorpamplona/claude/blissful-babbage-fFHRt
Fix inverted guard in TagArrayBuilder.addUniqueValueIfNew
2026-06-01 15:23:04 -04:00
Claude 1c2775c6b5 fix(quartz): emit q tags again (inverted guard dropped all quotes)
TagArrayBuilder.addUniqueValueIfNew had an inverted guard:

    if (tag.has(1) || tag[0].isEmpty() || tag[1].isEmpty()) return this

Since has(index) == size > index, `tag.has(1)` is true for every
well-formed tag with a value, so the function returned early and never
added it. addUniqueValueIfNew / addAllUniqueValueIfNew are used only by
the quote() / quotes() builders, so every `q` tag (naddr, nevent, note,
nembed, npub, nprofile) has been silently dropped since this file was
introduced. Restore the missing `!` and add a regression test covering an
addressable (naddr) quote plus the guard's accept/skip/dedupe semantics.

https://claude.ai/code/session_01NMavNzJ7VRLhoD3hboCCC7
2026-06-01 19:15:03 +00:00
Vitor PamplonaandGitHub 877d401e34 Merge pull request #3120 from vitorpamplona/claude/laughing-noether-2EmMZ
Move HTML parsing and broadcast service to commons for KMP
2026-06-01 08:12:55 -04:00
Vitor PamplonaandGitHub 51503f278d Merge pull request #3119 from vitorpamplona/dependabot/github_actions/actions-bfa3075405
chore(actions): bump the actions group with 2 updates
2026-06-01 08:11:42 -04:00
nrobi144andClaude Opus 4.6 1b17ce6975 fix(desktop): wire like and zap on comment items
- Fix like: read replyNote.event inside lambda (not captured val)
  to avoid stale null reference. Consume reaction into local cache.
- Wire zap on comments: uses zapNote (now internal) with 21 sats default
  via NWC connection, same flow as main action row
- Wire like/zap in both FeedScreen (inline expansion) and ThreadScreen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 12:20:01 +03:00
nrobi144andClaude Opus 4.6 4b021351d3 fix(desktop): wire comment reactions + fix related content click navigation
- Wire onLike on CommentItem: ReactionAction.reactTo + broadcast
- Related content clicks use overlay navigation (ThreadScreen) since
  related notes may not be in the feed LazyColumn
- Add onNavigateToThreadOverlay param to ExpandedNoteContent
- Zap from comments deferred (requires full NWC flow)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 12:13:33 +03:00
nrobi144andClaude Opus 4.6 3f862637d1 fix(desktop): load comment author metadata on inline expansion
- Observe note.flow().replies so replyNotes recomputes when replies arrive
- Use loadMetadataBatched with explicit author pubkeys from reply events
- DisposableEffect for proper flow cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 12:09:28 +03:00
nrobi144andClaude Opus 4.6 08c7b5f214 fix(desktop): remove auto-scroll on card expansion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 12:08:35 +03:00
nrobi144andClaude Opus 4.6 4fddfef5dd feat(desktop): inline card expansion in feed
- Add expandedNoteId state to FeedScreen — clicking a card expands it
  in-place instead of navigating to separate ThreadScreen
- AnimatedVisibility(expandVertically + fadeIn) for smooth expansion
- ExpandedNoteContent composable renders CommentsCard + RelatedContentSection
  below the expanded card within the same LazyColumn item
- Auto-scroll expanded card to top of viewport
- Thread reply subscriptions start on expand, cancel on collapse
- Only one card expanded at a time — clicking another collapses current
- Search bar stays visible (floating header above LazyColumn)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 12:03:16 +03:00
nrobi144andClaude Opus 4.6 92e210a584 fix(desktop): follow pill visibility, metadata loading, reply + view all wiring
- Fix follow pill layout: author row uses weight(1f) so pill has room
  (was invisible due to SpaceBetween squeezing)
- Fix comment metadata: observe metadataState so author info recomposes
  when kind:0 arrives from relay
- Wire "View all" on related content to navigate to author profile
- Wire reply button on CommentItem to open reply compose dialog

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 11:45:32 +03:00
nrobi144andClaude Opus 4.6 5aa2f519e7 feat(desktop): visual overhaul of thread detail view matching Layers design
- Create CommentsCard: OutlinedCard with "Comments N" header + badge,
  "Most recent" label, reply input slot, comment items slot
- Create CommentItem: lightweight comment row with avatar, name, handle,
  time, content, Reply/Like/Zap actions (replaces heavy FeedNoteCard for replies)
- Restyle InlineReplyInput: cyan "Send" pill button instead of plain icon
- Revise RelatedContentRow: image-overlay cards (200x140dp) with AsyncImage
  background, dark gradient overlay, white title + author + zaps
- Restructure ThreadScreen: root note card → CommentsCard → Related section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 11:34:18 +03:00
nrobi144andClaude Opus 4.6 9194dac8f9 feat(desktop): related content section in thread view
- Create CompactNoteData @Immutable data class in commons for reuse
- Create RelatedContentSection composable with horizontal LazyRow
- Scan LocalCache for hashtag-matching + same-author notes
- Compact cards (160dp) with title, author, zap count
- Wire into ThreadScreen below reply notes
- Hidden when no related content found
- Subscriptions cancel on dispose

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 11:07:31 +03:00
nrobi144andClaude Opus 4.6 25c9cf4611 feat(desktop): share menu with copy/broadcast options
- Create ShareMenu composable with ShareMenuState
- 6 share options: Copy Text, Copy Note ID, Copy Event Link, Copy Raw JSON,
  Copy Web Link (njump.me), Broadcast
- Replace MoreVert overflow menu with Share icon + ShareMenu
- Use existing copyToClipboard helper for clipboard operations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 11:07:19 +03:00
nrobi144andClaude Opus 4.6 cc1330adb6 feat(desktop): inline reply in thread view
- Create InlineReplyInput composable (avatar + TextField + Send button)
- SendState sealed interface (Idle/Sending/Error)
- Ctrl/Cmd+Enter keyboard shortcut to send
- Build kind:1 reply with NIP-10 e-tag + p-tag
- Optimistic display via localCache.consume + broadcastToAll
- Error shown inline with text preserved for retry
- Hidden for logged-out users

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 11:06:50 +03:00