Commit Graph

4815 Commits

Author SHA1 Message Date
Claude bb0f41d320 docs: mark onchain zaps Phase D (send flow) shipped 2026-05-14 12:31:11 +00:00
Claude 40ccffee35 feat: NIP-BC onchain zap send flow (Phase D)
Wires the Phase A.2 quartz send-side foundation into a working end-to-end
send: build → sign → broadcast → publish kind:8333.

commons/onchain/OnchainZapSender — stateless orchestrator. Loads the
sender's UTXOs, builds the unsigned PSBT via OnchainZapBuilder, signs it
through NostrSigner.signPsbt, finalizes + broadcasts via OnchainBackend,
then publishes the kind:8333 receipt through an injected publish callback.
Per-stage failures surface as OnchainZapSendResult.Failure (with the
broadcast txid preserved when only the receipt publish failed) instead of
throwing; CancellationException always propagates.

Account.sendOnchainZap — thin wrapper binding the account's signer, the
LocalCache.onchainBackend, and signAndComputeBroadcast into the
orchestrator.

amethyst wallet UI:
- OnchainZapSendDialog — collects recipient npub (or a fixed recipient when
  launched from a note's zap menu), amount, fee tier (slow/normal/fast from
  the backend's fee estimates), and an optional comment; runs the send and
  shows progress + success/failure.
- OnchainSection — the Bitcoin card on the wallet screen gains a "Send"
  button next to "Copy address" that opens the dialog for a profile zap.

Tests: OnchainZapSenderTest covers the happy path (receipt references the
broadcast txid, recipient, and amount), insufficient-funds failure at the
build stage, broadcast failure (no txid leaked), and publish failure (txid
preserved for retry).

Pending: Phase B (NIP-55 sign_psbt Intent) — external/remote signers still
throw UnsupportedMethodException; note-zap-menu entry point can reuse
OnchainZapSendDialog's recipientPubKey/zappedEvent parameters once wired.
2026-05-14 12:29:31 +00:00
Claude f5e7240425 docs: mark onchain zaps Phase A.2 (send-side foundation) shipped 2026-05-14 11:32:15 +00:00
Claude bb5613ca8b docs: update onchain zaps plan — A.1 + C shipped, A.2/B/D pending
Marks the receive + display phases done and spells out exactly what
Phase A.2 (send-side PSBT foundation) needs before send can ship —
notably the fund-safety-critical BIP-174 codec and BIP-341 tweaked-key
signing path.
2026-05-14 04:59:23 +00:00
Claude 6ce193d46a feat(amethyst): show onchain balance on wallet screen Bitcoin section
OnchainSection now fetches UTXOs for the derived Taproot address from the
configured Esplora backend and displays the sum as the section balance.
Loading / unavailable / error states are surfaced inline. No retry on
failure — the user refreshes by re-entering the screen.

Balance fetch runs in a LaunchedEffect keyed on the derived address (which
is keyed on the account pubkey), so it kicks off once per account view.
2026-05-14 04:58:18 +00:00
Claude 031159b3f6 feat(amethyst): NIP-BC onchain zap receive + display
Phase C of the onchain zaps plan (amethyst/plans/2026-05-14-onchain-zaps.md).
Plumbs the Quartz receive foundation from Phase A.1 into the Android app so
incoming kind 8333 zaps show up in note totals and the wallet screen.

Subscription / fetch path (kind 8333 rides existing zap filters, no new
assemblers per the plan):
- FilterRepliesAndReactionsToNotes: OnchainZapEvent.KIND in
  RepliesAndReactionsKinds (#e on notes)
- FilterUserProfileZapReceived: OnchainZapEvent.KIND in
  UserProfileZapReceiverKinds (#p on profiles)
- UserProfileZapsViewModel: kinds = LnZap + Onchain inline
- FilterNotificationsToPubkey: OnchainZapEvent.KIND in SummaryKinds
- NotificationFeedFilter / NotificationDispatcher: kind 8333 in
  NOTIFICATION_KINDS
- FilterMessagesToLiveStream / FilterGoalForLiveActivity: kind 8333
  alongside 9735

Display path (Note.zapsAmount fold-in — no UI changes needed downstream):
- commons/model/Note.kt: new `onchainZaps: Map<String, OnchainZapAmount>`
  (keyed by txid for `(txid, target)` dedup); `addOnchainZap(txid, sats,
  confirmed)` mutator; `updateZapTotal` now also sums confirmed onchain
  sats into `zapsAmount`. Pending entries are tracked but excluded from
  the aggregate total per the NIP-BC spec.
- commons/model/OnchainZapAmount.kt: new data class for per-tx state.
- LocalCache: new `onchainBackend: OnchainBackend?` slot; new
  `consume(OnchainZapEvent)` handler that runs the same sig-verify and
  computeReplyTo pipeline as `consume(LnZapEvent)`, then kicks off async
  verification on `applicationIOScope`. Self-zaps are rejected
  synchronously per the spec. Verification failures, mempool-only txs,
  and confirmed txs all route to `Note.addOnchainZap` with the correct
  flag. Without a backend wired, events are still cached (so subscriptions
  see them) but skip the total contribution.
- LocalCache.computeReplyTo: new `is OnchainZapEvent ->` branch that
  enumerates `e`/`a` targets — profile-only zaps return an empty list and
  flow through profile zap queries instead.

Wallet UI:
- Wallet screen now renders a "Bitcoin" Card above the existing NIP-47
  NWC wallet list. Single card (one onchain wallet per Nostr identity,
  derived from the pubkey). Shows the bc1p taproot address + Copy button.
  Balance, recent incoming zaps, and tap-to-detail come in later phases.

Settings + wiring:
- AccountSettings: new `onchainEsploraEndpoint: MutableStateFlow<String>`,
  default mempool.space, plumbed for a future settings UI.
- AppModules: builds a single shared `EsploraBackend` at app init using
  the role-based money-flavored OkHttp client (Tor-aware), installs it on
  `LocalCache.onchainBackend` so verification can run.

Secp256k1Instance: new `pubKeyTweakAdd` was added in Phase A.1.

No PSBT, no signing, no broadcasting — send-side (Phase A.2 + D) is still
pending.
2026-05-14 04:54:40 +00:00
Claude 79f1d43581 feat(quartz): NIP-BC onchain zap receive-side foundation
Phase A.1 of the onchain zaps plan (amethyst/plans/2026-05-14-onchain-zaps.md):
the verify/receive path of NIP-BC. Send-side (PSBT, builder, signPsbt) is the
next phase.

Adds:
- nipBCOnchainZaps/taproot/SegwitAddress: BIP-173 / BIP-350 native segwit
  address encoder/decoder, witness v0..v16, on top of the existing Bech32 util.
- nipBCOnchainZaps/taproot/TaprootAddress: BIP-341 key-path-only derivation
  from a Nostr pubkey. Computes the TapTweak tagged hash, applies the additive
  tweak via Secp256k1Instance.pubKeyTweakAdd, and encodes as bc1p... bech32m.
- Secp256k1Instance: new pubKeyTweakAdd primitive wired through commonMain
  expect + jvm / android / native actuals.
- nipBCOnchainZaps/chain: pluggable OnchainBackend interface plus BitcoinTx,
  BitcoinTxOutput, Utxo, FeeEstimates data models.
- nipBCOnchainZaps/chain/EsploraBackend (jvmAndroid): OkHttp-based
  Esplora-compatible client (mempool.space / blockstream.info) covering
  /tx, /address/{addr}/utxo, /tx broadcast, /blocks/tip/height, and
  /v1/fees/recommended.
- nipBCOnchainZaps/verify/OnchainZapVerifier: enforces every NIP-BC client
  rule -- reject self-zap, fetch tx, derive recipient Taproot scriptPubKey,
  sum only outputs paying the recipient (excluding change back to sender),
  cap claimed amount at verified amount, mark unconfirmed as Pending.

Tests (commonTest, runs on jvmTest):
- SegwitAddressTest: BIP-350 known-good vectors for v0/v16 + round-trip for
  v1 (P2TR).
- TaprootAddressTest: BIP-341 wallet-test-vectors tagged hash + tweaked
  output key + scriptPubKey, plus address round-trip and prefix/length
  sanity.
- OnchainZapVerifierTest: confirmed / pending / self-zap / missing tx /
  zero-verified-amount / multi-output-sum / amount-capping with an
  in-memory FakeBackend wired to real TaprootAddress-derived scripts.

Plan: amethyst/plans/2026-05-14-onchain-zaps.md documents the full v1 scope,
the merge into the existing NIP-47 wallet UI, the subscription kind-list
edits, and the Note.zapsAmount fold-in approach for display.
2026-05-14 04:34:30 +00:00
Vitor Pamplona 17f207bb31 Merge pull request #2882 from vitorpamplona/claude/fix-dialog-paging-dots-yCTp5
fix: pad zoomable dialog pager dots above gesture/nav bar
2026-05-13 23:02:32 -04:00
Claude 12650bd7fd feat: show bottom nav on Favorite Algo Feeds screen
Switch FavoriteAlgoFeedsListScreen from a plain Material Scaffold to
DisappearingScaffold and host an AppBottomBar bound to
Route.EditFavoriteAlgoFeeds, matching the pattern used by every other
tab-root feed (Badges, Articles, Bookmark Groups, ...). Hoist the
LazyColumn state so re-tapping the bar item scrolls to top.

When the user pins this screen as a bottom-bar entry, navigation lands
here via nav.navBottomBar() which marks the entry as a tab root, so
AppBottomBar renders and TopBarWithBackButton hides its back arrow.
When reached as a pushed screen from elsewhere, AppBottomBar's existing
canPop() guard hides the bar and the back arrow stays — preserving the
old behaviour for non-bottom-bar entry points.
2026-05-14 02:53:02 +00:00
Claude d9ce949eda fix: pad zoomable dialog pager dots above gesture/nav bar
The page indicator in the image/video zoom dialog sat too low on
Android devices, getting covered by the gesture bar or 3-button nav.
Apply navigationBarsPadding() to the indicator surface so it floats
above the system bar inset.
2026-05-14 02:03:18 +00:00
Vitor Pamplona f3574081b5 Merge pull request #2879 from mstrofnone/docs/nip9b-renumber-comments
docs(community-rules): rename NIP-9A -> NIP-9B (upstream slot collision with #2194)
2026-05-13 21:48:02 -04:00
Vitor Pamplona 04743f700b Merge pull request #2878 from vitorpamplona/claude/modernize-security-filters-ui-IPUjW
Refactor SecurityFiltersScreen into modular settings screens
2026-05-13 19:57:57 -04:00
m 2c3d006f05 docs: rename NIP-9A -> NIP-9B in code comments + user strings
The upstream NIP draft for kind:34551 community rules
(nostr-protocol/nips#2331) was renumbered from 9A to 9B per maintainer
feedback that slot 9a is already claimed by the push-notifications
draft (nostr-protocol/nips#2194):

  https://github.com/nostr-protocol/nips/pull/2331#issuecomment-4442813289

This commit updates all human-readable references in the merged
community-rules code:

- 7 Kotlin files in quartz (CommunityRulesEvent, CommunityRulesValidator,
  5 tag classes) — Kdoc comments
- 13 Kotlin files in amethyst (composer, feed filter, rules editor,
  Account, AccountSettings, tests) — code comments + Kdoc
- 1 English strings file (values/strings.xml) — 2 user-facing strings
- 54 translation strings files (values-*-r*/strings.xml) — same two
  strings, untranslated "NIP-9A" token replaced with "NIP-9B"

Kind number (34551), schema, tag names, and behaviour are unchanged.
No public API or DTO field renames. Pure docs/strings.

Verified: :quartz:spotlessCheck, :amethyst:spotlessCheck,
:commons:spotlessCheck all clean.
2026-05-14 09:29:59 +10:00
Claude 0b7399feb0 fix: pad Loading/Error states and tighten Stepper resync
- Wrap the when-branches in SelectableUserList, HiddenWordsList, and
  MutedThreadsList in Box(modifier.fillMaxSize()) so the Scaffold
  padding is applied to every state — Loading/Error/Empty/Loaded — not
  just the LazyColumn. Fixes a regression where the loading spinner
  and error UI rendered under the top bar.
- SettingsStepper now clamps `value` once into `[min, max]` and uses
  the raw `value` (re-clamped) in the +/- handlers. If the model
  starts below `min`, the first tap of `+` resyncs it to `min`
  instead of jumping `min+1` (skipping a step). Display still falls
  back to `unsetLabel` when `value <= 0`.
- WarnReportsTile no longer pre-coerces threshold to >= 1 at the call
  site; the stepper handles it.
- EmptyState drops its now-unused modifier parameter.
2026-05-13 23:09:06 +00:00
Claude 06e88cd30e refactor: address second-pass Security Filters audit
- Restore pull-to-refresh on BlockedUsers and SpammingUsers by wrapping
  SelectableUserList in RefresheableBox (regression from the first
  refactor; HiddenWords and MutedThreads remain non-pull-refresh to
  match the original behavior).
- Drop the redundant transientHiddenUsers subscription in
  InvalidateOnBlockListChange — hiddenUsers.flow already combines it.
  Also drop the meaningless accountViewModel key on LaunchedEffect.
- Move CountBadge and Stepper into SettingsSectionCard as
  SettingsCountBadge and SettingsStepper so other settings screens can
  reuse them. Widen the stepper value cell to min 40.dp so three-digit
  values like 999 fit comfortably.
- Drop the unused `enabled` flag from SettingsControlRow (sub-controls
  use SettingsSubControlRow for dimming). Tighten the docstring.
- Unify SwitchTile to take @StringRes Int params, matching the @StringRes
  annotations now applied to SettingsItem, SettingsSection, BlockListTopBar,
  SelectableUserList, EmptyState.
- Shorten property chains by binding `val security = …syncedSettings.security`
  per tile when more than one field is read; pass method references
  (accountViewModel::updateFilterSpam etc.) where possible.
- Hoist WarningType.entries to a local val so the segmented button row
  reads `count = options.size` instead of evaluating twice.
- MutedThreadsScreen: replace itemsIndexed with items (unused index) and
  use MaterialTheme.colorScheme.onPrimary instead of hardcoded Color.White
  for the "Unmute" button text.
- Add a @Preview for SecurityFiltersScreen, mirroring AllSettingsScreen.
2026-05-13 22:00:38 +00:00
Claude 6feca2cf17 refactor: address Security Filters audit findings
- Extract SettingsSection/SettingsItem/SettingsDivider/SettingsControlRow/
  SettingsBlockTile/SettingsSubControlRow into SettingsSectionCard.kt.
  AllSettingsScreen now consumes the shared components instead of carrying
  its own private copies, and SecurityFiltersScreen drops the parallel set
  it used to declare.
- BlockedContentRow folds into SettingsItem(trailing = { CountBadge(...) }),
  removing the bespoke navigation row.
- Replace the Spinner/Switch tile mix with WarnReportsTile = SwitchTile +
  SettingsSubControlRow(stepper), avoiding the nullable-icon code smell.
- Move the sensitive-content SegmentedButtonRow out of a cramped trailing
  slot and into SettingsBlockTile so it has full-width room.
- HiddenWordsScreen: replace the eager Column.forEach with LazyColumn, and
  add Modifier.imePadding() to the Scaffold so the keyboard no longer
  covers the Add-word field.
- Move SelectableUserList from BlockedUsersScreen.kt to SecurityListsCommon
  and drop the nullable onToggle (both call sites pass one).
- BlockListTopBar now takes selectedCount: Int instead of Set<*>; its
  UserFeedState branches are exhaustive (no else -> Unit fallthrough).
- CountBadge reserves min width so 0↔n transitions don't shift the row.
- Stepper relies on Material 3's enabled handling instead of computing
  alpha by hand and tinting LocalContentColor in three places.
- Distinct icons per tile/category (Visibility, Code, VisibilityOff, etc.)
  so the same MaterialSymbols.Tag isn't reused for unrelated rows.
2026-05-13 21:34:53 +00:00
Claude 2816ceb479 feat: split Security Filters into a hub with per-category screens
Replaces the cramped single-screen layout (header settings + four tabs)
with a hub that uses the same grouped-card pattern as AllSettingsScreen,
plus a dedicated screen per blocked-content category. The tile control
mix is also modernized:

- "Show sensitive content" is now a 3-way SegmentedButton instead of a
  free-floating spinner sitting awkwardly next to switches.
- Numeric prefs (report threshold, max hashtags) use a stepper (- / + )
  instead of free OutlinedTextFields with no commit affordance.
- The report-threshold tile renders inline under its toggle and dims
  when the toggle is off, instead of appearing/disappearing and shifting
  layout.
- Each tile is a row with leading icon, title, description, trailing
  control - so the spinner/switch/stepper visual rhythm stays consistent.

The hub's "Blocked content" section shows a count badge per category and
routes to its own screen (Routes.BlockedUsers, SpammingUsers, HiddenWords,
MutedThreads). The hidden-words screen now docks the "Add word" field as
a bottom bar so it doesn't compete with the list for vertical space, and
selection mode is scoped per-screen instead of shared across tabs.
2026-05-13 20:48:53 +00:00
Vitor Pamplona dbd7266d8f better description of the draft option 2026-05-13 16:29:17 -04:00
Claude 128048f043 perf: stop recomposing RenderScreen on every profile-tab swipe
viewedTab was a MutableState read inside RenderScreen at
visibleTabs.indexOf(viewedTab), so every pager swipe — which writes to
viewedTab from snapshotFlow — invalidated the enclosing restart group
even though rememberPagerState's initialPage is never re-applied after
the first composition.

Move the tracker to a plain holder class (ViewedTabRef). Reads inside
the key(visibleTabs) block no longer subscribe to snapshot changes, so
swipes only recompose the tab row / pager content as intended.
2026-05-13 19:50:52 +00:00
Claude 251699beaa fix: honor Profile UI toggles in subscriptions and pager state
- Gate WatchLifecycleAndUpdateModel(appRecommendations) on the
  showProfileAppRecommendations toggle so the viewmodel no longer
  refreshes when the section is hidden.
- Thread loadFollowers / loadZapsReceived through UserProfileQueryState
  and the matching sub-assemblers. When the user hides those tabs,
  UserProfileFilterAssembler stops emitting their relay filters and
  the live REQs drop.
- Re-pin the profile pager to the tab the user was viewing whenever
  the visible-tabs list changes, using key(visibleTabs) +
  rememberPagerState(initialPage = ...) and a snapshotFlow that tracks
  the currently-viewed ProfileTab. Prevents the pager from silently
  shifting to a different tab when one is toggled off in settings.
2026-05-13 19:50:52 +00:00
Claude f26f3626f1 feat: add Profile UI settings page
New "Profile UI" settings page lets users toggle which profile-screen
sections are loaded and displayed. All toggles default to on.

Toggles:
- Profile Badges (NIP-58 badges row)
- App Recommendations (apps row in profile header)
- Zap Received Feed (received zaps tab)
- Followers Feed (followers tab)

Tabs are filtered from the profile pager so disabled feeds no longer
allocate a page or fire their subscriptions.
2026-05-13 19:50:52 +00:00
Crowdin Bot 216ce24839 New Crowdin translations by GitHub Action 2026-05-13 19:45:26 +00:00
Vitor Pamplona 680aacf5b9 fix nests demoted speakers stuck on stage and broadcasting
Three related bugs when a host removes a speaker from the stage on
nostrnests by editing the kind-30312 to drop their p-tag:

1. NestRoomFilterAssembler didn't subscribe to the kind-30312 itself
   while in-room — only chat/presence/reactions (#a) and admin
   commands. Once joined, the room event was frozen on whatever
   version loaded the screen, so demotions never propagated. Added
   a per-relay filter on `kinds=[30312], authors=[host], #d=[dTag]`.

2. Even if the demotion did propagate, `BroadcastHandle` kept
   running. Only the manual Leave Stage button called
   `stopBroadcast()`. Added a LaunchedEffect that tears down the
   broadcast when the local user falls off `participantGrid.onStage`
   while still publishing — covers both demote-by-host and
   leave-stage-on-another-client.

3. The auto-stop reliably exercised a pre-existing
   NestForegroundService bug: when `startListening` was called to
   demote from mic+media to media-only, `intent.action == null`
   fell through to `else -> promoted`, keeping mic=true. The service
   then asked startForeground for FOREGROUND_SERVICE_TYPE_MICROPHONE
   after the mic had been released, threw SecurityException on
   Android 14+, runCatching swallowed it, stopSelf ran without
   startForeground → ForegroundServiceDidNotStartInTimeException.
   The explicit-demote branch now returns false; only intent==null
   (OS sticky-restart) preserves the prior promoted state.
2026-05-13 15:41:51 -04:00
Vitor Pamplona 20a2270434 fix nests room author shown in audience with no audio
nostrnests publishes kind-30312 without a self-`p`-tag for the room
author — they're the implicit host. The lobby card already handles
this (NestJoinCard) but the in-room code did not: the author fell
into the pure-audience branch with role=null AND was missing from
the audio subscription set, so the host appeared as a regular
listener and no sound played.

buildParticipantGrid now takes hostPubkey and synthesizes a virtual
`["p", host, "", "host"]` when absent, so the existing presence-aware
onstage/audience rules apply uniformly — including "host leaves
stage" (onstage=0 → drops to audience, role stays HOST).

NestActivityContent owns the grid now and derives onStageKeys from
it, so the StageGrid render and the MoQ subscription set stay in
lockstep when anyone (host or speaker) steps off stage.
2026-05-13 15:41:51 -04:00
Vitor Pamplona e7dc8ec25c Merge pull request #2875 from vitorpamplona/claude/timeago-toggle-format-Y3lGX
Make timestamps toggleable between relative and absolute formats
2026-05-13 15:40:10 -04:00
Claude 66202e667d refactor: switch Compose Settings toggles to Material Switch
Each binary setting (auto-create drafts, AI writing help, tracked
broadcasts) becomes a single-tap Switch instead of a two-tap
Always/Never spinner, and a shared BooleanSwitchRow helper removes the
per-toggle boilerplate.

https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym
2026-05-13 19:36:53 +00:00
Claude 4d2886d162 refactor(timeago): consolidate toggle into one stable composable
Audit follow-up — the toggle behaviour now lives in a single
`ToggleableTimeAgoText` core in `ui/note/elements/TimeAgo.kt`. `TimeAgo`,
`NormalTimeAgo`, `ChatTimeAgo`, and `ChatroomHeaderCompose.TimeAgo` are
thin wrappers that pick a `TimeAgoStyle` (Dotted / Short) and pass
colour/font params; no per-site duplication of state + clickable +
derivedStateOf.

Performance fixes that matter for a feed with hundreds of timestamps:

- `rememberSaveable` → `remember`. Persisting a transient peek-toggle to
  the SavedStateRegistry for every visible+scrolled-past note was pure
  memory bloat. Recycling now resets to relative, which is the expected
  behaviour for a transient inspect action.
- The relative-mode `derivedStateOf` lambda reads `nowState.value` only
  when displaying a relative time. An item the user has frozen to its
  absolute date no longer re-evaluates every 30-second tick.
- Core composable takes only stable primitive params (Long, Color,
  TextUnit, TextOverflow, enum) so Compose can skip it entirely when
  inputs don't change.
- Desktop wrapper dropped the redundant `derivedStateOf`: its formatter
  reads no State, so derivedStateOf had nothing to observe.

https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
2026-05-13 19:31:15 +00:00
Claude d5860841c9 refactor: gate only draft create, and colocate compose composables
- sendDraftSync no longer short-circuits the delete-on-blank path when
  automatic draft creation is disabled. Clearing a composer now always
  cleans up any existing draft; the setting only suppresses creation.
- Physically move AiWritingHelpChoice and TrackedBroadcastsChoice from
  AppSettingsScreen.kt into ComposeSettingsScreen.kt so the composable
  definitions live alongside the screen that hosts them.

https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym
2026-05-13 19:28:27 +00:00
Claude 38463e5f2c feat: tap TimeAgo to toggle relative ↔ absolute timestamp
Every TimeAgo composable (Android NoteCompose timestamp, NormalTimeAgo,
ChatTimeAgo, ChatroomHeaderCompose last-message time) and every Desktop
timestamp Text (NoteCard, NotificationsScreen, ChatPane, ConversationListPane)
is now clickable and toggles to a scale-adjusted absolute date/time:

  - same day → time only (e.g. "14:32"), locale-aware
  - same year → "MMM dd, HH:mm"
  - older    → "MMM dd, yyyy"

State is hoisted per-call site via rememberSaveable so the toggle survives
scroll-induced disposal in lazy lists. Desktop call sites share a small
ToggleableTimeAgoText wrapper; Android keeps its existing composable shapes
and just gains a clickable modifier + state.

https://claude.ai/code/session_01AuPon9VQeRfKV1BTVQuKGC
2026-05-13 19:17:55 +00:00
Claude 1b8b508816 feat: add Compose Settings screen with auto-draft toggle
Introduces a new Compose Settings screen that groups composer-related
preferences. Adds a new "Automatically create drafts" toggle that gates
the automatic draft creation triggered on back-press / cancel across
every composer (short notes, public messages, long-form, classifieds,
public channels, private DMs, nests).

Moves "Propose text improvements" and "Tracked broadcasts" out of
Application Preferences and into the new Compose Settings screen.

https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym
2026-05-13 19:04:33 +00:00
Vitor Pamplona 781e8484ac force video box to be there when the video is still building 2026-05-13 12:35:37 -04:00
Vitor Pamplona 5cf3a08b92 Correct way to show the video's blurhash when loading 2026-05-13 12:35:37 -04:00
Vitor Pamplona 51cb12e563 Merge pull request #2872 from vitorpamplona/claude/fix-shortnewpost-relays-pTAzG
Refactor relay broadcast logic for personal and channel events
2026-05-13 11:44:22 -04:00
Crowdin Bot a119da177f New Crowdin translations by GitHub Action 2026-05-13 15:17:11 +00:00
Crowdin Bot a76237e1ad New Crowdin translations by GitHub Action 2026-05-13 15:04:44 +00:00
davotoula 8585b0fcf4 i18n: translate muted-threads strings into cs/de/pt-BR/sv
Adds Czech, German, Brazilian Portuguese, and Swedish translations
for the 6 new muted-threads keys introduced with the mute-thread
feature (action_unmute, quick_action_mute_thread,
quick_action_unmute_thread, settings_muted_threads_empty,
settings_muted_threads_title, settings_muted_threads_unknown).
2026-05-13 17:01:18 +02:00
davotoula 9632325349 Code review:
- tighten placeholder dictionary after review
- Skip the placeholder scan when the source has no `{` (avoids Matcher
  allocation on the common path).
- Clamp `firstFreeIndex` to PLACEHOLDER_LIMIT so adversarial user text
  like `{99999}` can't push our counter past the table limit and make
  placeholder() throw for the rest of the note. New JVM test covers it.
- split clamp and max-update in firstFreeIndex
2026-05-13 15:49:08 +02:00
davotoula d943fc5f49 fix(translation): use {N} placeholders so URLs survive CJK translation
PUA codepoint placeholders (+) were being dropped by ML Kit's
Japanese/Chinese/Korean models, eliding image URLs from translated text
(issue #1180). On-device probing across 8 source languages and 12
placeholder styles shows ICU MessageFormat tokens `{0}`, `{1}`, … are
the only style preserved intact by every model we tested.

build() now scans the source for any user-supplied `{N}` and starts the
dictionary counter above the max, so a note containing `printf("%s", arg{0})`
can't be clobbered by encode/decode. Adds an on-device regression test
with the exact post from the bounty issue and JVM tests for the new
collision-avoidance behaviour.
2026-05-13 15:29:57 +02:00
Vitor Pamplona 16e87cea8d Merge pull request #2862 from davotoula/feat/161-mute-thread
Client-side thread mute (NIP-51 kind-10000 e tags)
2026-05-13 08:14:50 -04:00
Vitor Pamplona 00f9222227 Merge pull request #2863 from davotoula/fix/sonar-issues
Sonar cleanup — mechanical refactors only
2026-05-13 08:14:07 -04:00
Vitor Pamplona ac876ab11a Merge pull request #2866 from greenart7c3/claude/fix-blossom-cache-url-M9Pzk
Fix Blossom bridge to only rewrite last path segment as SHA256
2026-05-13 08:13:35 -04:00
Vitor Pamplona c32b30e27f Merge pull request #2867 from greenart7c3/claude/fix-home-tab-dot-oi1g0
Refactor homeHasNewItems to respect tab visibility settings
2026-05-13 08:12:40 -04:00
Claude c7eba6a620 fix(home): clear Home dot when any one home tab is read to the top
The previous attempt lit the dot whenever *any* enabled home tab had unread
items, so a user with all three tabs enabled who only scrolled through the
Everything tab still saw the dot — HomeFollows / HomeFollowsReplies stayed at
their old lastRead even though the same content had been read via Everything.

Switch to "every enabled tab still has unread" — equivalently, reaching the
top of any single enabled tab clears the dot. This matches the pre-bug
behavior (where only the New Threads tab gated the dot) while still respecting
users who only enable the Everything tab.
2026-05-13 11:01:08 +00:00
Claude 3cc9ac8b7c fix(blossom-bridge): only the last path segment is the blob hash
BUD-01 defines a Blossom URL as `<server>/<sha256>[.<ext>]` — the blob hash is
always the last path segment. Walking the path right-to-left for any hex
match was too permissive: a non-Blossom URL like `https://example.com/<sha>/avatar.jpg`
(sha appears in an intermediate segment) would get incorrectly bridged.

Both parsers now look at the last path segment only and skip the URL entirely
when it isn't a sha256. The earlier hex-prefix case (share.yabu.me's
`<cache-prefix>/<blob>.ext`) still works because the blob is still the last
segment; the prefix flows into `xs` via the existing `buildServerBase` /
`extractServerBase` logic. Adds negative tests covering the
sha-in-non-last-segment case in both modules.
2026-05-13 10:27:26 +00:00
Claude 74c2bd2fa8 test(blossom-bridge): inline the share.yabu.me URL literally
Building the URL from extracted prefix/blob constants could mask a parser bug
that splits the path the same way the test constructs it. Use the full URL
from the bug report as a single literal so the test only agrees with the
parser if the parser actually parses the URL correctly.
2026-05-13 10:21:59 +00:00
Claude 1b287baaf2 fix(blossom-bridge): pick rightmost sha256 segment in URL path
CDNs like share.yabu.me serve blobs under `<cache-prefix-sha>/<blob-sha>.<ext>`
where both path segments are 64-char hex. The bridge previously locked onto the
first match (the cache prefix), dropping the blob segment from `xs` and asking
the local cache for a non-existent blob. Walking the path right-to-left makes
the rightmost sha — the one that carries the file extension — win, while the
prefix segments stay in `xs` so the cache can fetch upstream on miss.

Behaviour is unchanged when the path has a single sha; tests cover the new
two-hash layout in both the Coil-model path and the OkHttp interceptor path.
2026-05-13 10:11:32 +00:00
Crowdin Bot fb74d5e0f1 New Crowdin translations by GitHub Action 2026-05-13 08:55:53 +00:00
davotoula 8ed7e63427 refactor(sonar): merge chained if-else into when
Sonar — convert 'if (A) {…} else if (B) {…} else {…}' chains into
'when { A -> …; B -> …; else -> … }' across 33 files (~44 sites).
The change is mechanically equivalent: same conditions, same branch
order, same short-circuit evaluation, no public-signature impact.

The 70-branch event-type dispatch in ThreadFeedView.kt:594 is intentionally
left as if-else for now — the conversion would balloon to a 280-line diff
with no behavioural change, making review impractical.
2026-05-13 10:40:38 +02:00
davotoula 5e179326d4 refactor(sonar): replace if/throw with require/error
Sonar — replace hand-rolled 'throw IllegalArgumentException' / 'throw
IllegalStateException' with the idiomatic 'require { msg }' / 'error(msg)'.
Exception types preserved exactly (require throws IAE, error throws ISE).
2026-05-13 10:17:13 +02:00
davotoula 6960bdf497 refactor(sonar): hoist return before if-else
Sonar — collapse 'if (X) { ...; return A } else { ...; return B }' to
'return if (X) { ...; A } else { ...; B }' across 5 files. Mechanically equivalent.

PlaybackService.lazyPool keeps its in-branch non-local returns from .let{} blocks
(those are early-out cache hits, not tail returns).
2026-05-13 10:15:26 +02:00