Commit Graph

13511 Commits

Author SHA1 Message Date
Claude cd28de4eb1 fix(nests): cap AudioTrack ring at ~250 ms so audio tracks the speaking-now ring
`AudioTrackPlayer` sized the AudioTrack ring at `max(minBuffer * 16,
250 ms)`. The intent (per the kdoc) was ~250 ms of jitter slack with
the device's `getMinBufferSize` as a floor — but the `* 16` multiplier
silently dominated on common devices. A Pixel reports `minBuffer ≈
40 ms` for 48 kHz mono; `× 16 = 640 ms`. Add the 200 ms preroll in
`NestPlayer` and decoded PCM sat in the ring for ~800 ms – 1 s before
the speaker played it.

Two-phone testing confirmed the symptom: the speaking-now ring lit up
~1 s before the listener's audio. The ring fires on
`onLevel(peakAmplitude(pcm))` in `NestPlayer.play()` immediately after
`decoder.decode()` succeeds — before `player.enqueue(pcm)` — so the
ring is real-time and the delay was entirely between `enqueue` and
the speaker. The web (NostrNests) listener uses an `AudioWorklet`
with no equivalent ring buffer, so its audio aligned with the wire.

Drop the `* 16` so the formula matches the intent: `max(minBuffer,
250 ms)`. On devices whose floor is below 250 ms (the common case)
the 250 ms target wins; on devices whose floor is above 250 ms (rare)
we still respect the device-reported minimum. Steady-state ear-to-
speaker delay drops from ~1 s to ~450 ms (250 ms ring + 200 ms preroll).
2026-05-14 03:25:51 +00:00
Claude dec56c2ee9 Revert: remove NestsTrace listener-pipeline instrumentation
Removes the four trace points + debug-build auto-enable hook added in
the prior commit on this branch. They served their purpose locally
(confirmed the ~1s startup lag lives between pcm_decoded and
pcm_enqueued, i.e. AudioTrack ring backpressure), and don't need to
land in main.
2026-05-14 03:23:02 +00:00
Claude 86c66920d3 chore(nests): instrument listener audio pipeline with NestsTrace
Adds four trace points on the listener pipeline so an end-to-end JSONL
capture (`adb logcat -s NestsTraceJsonl:D -v raw > nest-trace.jsonl`)
shows wall-clock latency at every stage:

  frame_received       (MoqLiteSession.drainOneGroup, after trySend)
  frame_object_mapped  (MoqLiteNestsListener.wrapSubscription map)
  pcm_decoded          (NestPlayer.play, after decoder.decode)
  pcm_enqueued         (NestPlayer.play, after player.enqueue)

Each event carries (sub_id, group_seq, ...) or (track_alias, obj_id, ...)
so the same frame can be followed through all four stages. Deltas
isolate which segment owns the observed startup-only ~1s lag:

  subscribe_send → first frame_received : relay forward latency
  frame_received → pcm_decoded          : Opus decode wall-time
  pcm_decoded   → pcm_enqueued          : AudioTrack ring backpressure
  pcm_enqueued  → next pcm_decoded      : decode-loop scheduling

Tracing auto-enables on debug builds when `NestActivity` opens and
disables on `onDestroy` so release apps pay only the field-load
guard inside `NestsTrace.emit`. Capture instructions are inline at
the call site in `NestActivity.onCreate`.
2026-05-13 23:09:31 +00:00
Vitor Pamplona dbd7266d8f better description of the draft option 2026-05-13 16:29:17 -04:00
Vitor Pamplona 8dd4ed4c6d Merge pull request #2877 from vitorpamplona/claude/add-profile-settings-page-ezkgF
Add profile UI settings to customize visible sections and feeds
2026-05-13 15:56:26 -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
Vitor Pamplona 6b06db9395 Merge pull request #2876 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 15:49:21 -04: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
Vitor Pamplona 487e91eff6 Merge pull request #2874 from vitorpamplona/claude/add-compose-settings-IthDX
Add compose settings screen with auto-draft creation toggle
2026-05-13 15:40:01 -04:00
Vitor Pamplona 0618f8ded7 Merge pull request #2873 from vitorpamplona/claude/review-amethyst-issues-ePExg
Round-5 performance & security: frame decode, HP, AEAD, resumption
2026-05-13 15:39:30 -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 8a3bd52631 fix(quic): finish P1 + A2 audit follow-ups (PR #2873)
P1 (header protection — eliminate remaining HP allocations):
Add HeaderProtection.maskInto(hpKey, src, srcOffset, scratch16, dstMask)
that writes the 5-byte mask into a caller-owned buffer using a
caller-owned 16-byte AES scratch. Add `hpScratch` (16 bytes) and
`hpMask` (5 bytes) to PacketProtection — same per-direction
single-threaded analysis as nonceScratch from the prior commit. Thread
both buffers through Short/LongHeaderPacket build / parseAndDecrypt /
peekKeyPhase as optional parameters paired with nonceScratch; production
call sites in QuicConnectionWriter / QuicConnectionParser pass
`proto.hpScratch` + `proto.hpMask`. With both this commit and 09b28b8d
the AES-128-GCM hot path now allocates ZERO bytes for HP + nonce per
packet (down from 16-byte sample slice + 16-byte AES output + 5-byte
mask + 12-byte nonce = 49 bytes/pkt).

ChaCha20HeaderProtection.maskInto routes through the same shape but
still allocates a fresh 5-byte ciphertext + 12-byte nonce slice
internally — Quartz's `ChaCha20Core.chaCha20Xor` SPI takes a
standalone nonce ByteArray. Documented as a future cleanup; AES is
the dominant path in production.

A2 (TlsRunningSha256 — lazy fallback accumulator):
Probe `digest.clone()` ONCE at construction. Conscrypt's clone
support is a build-time property (native bridge presence), not state-
dependent, so a single probe is a reliable signal. Devices where the
probe succeeds skip the byte accumulator entirely (zero overhead);
devices where it fails get the byte-accumulator path from the very
first update so the first snapshot already has the complete
transcript. Replaces the previous "always accumulate" shape that
wasted a few KB per handshake on every device, including the
overwhelming majority that support clone.

All quic JVM unit tests pass; spotless applied.

https://claude.ai/code/session_01EBHtGLy5o7FUR5qfcpHUUx
2026-05-13 19:27:47 +00:00
Claude 09b28b8d78 fix(quic): address self-audit findings on #2861 fixes (PR #2873)
Addresses three issues found in the self-audit of commit fbacef1f:

S2 clock bug (CRITICAL): the previous fix compared TlsResumptionState.
issuedAtMillis (wallclock — stored via System.currentTimeMillis() in
TlsClient) against QuicConnection.nowMillis() (monotonic — anchored at
construction). On any new connection the monotonic clock starts near
zero, so `(nowMillis() - issuedAtMillis)` produced a large negative
value, the coerceAtLeast(0L) clamped it to 0, and the expiry check
never fired. Add a dedicated `epochMillis: () -> Long` parameter on
QuicConnection (default System.currentTimeMillis()) and use that for
ticket-age comparisons — matches the source TlsClient stamps the
ticket with.

S3 null-ALPN filter: the effectiveResumption filter rejected ANY
ticket whose cached negotiatedAlpn was null. That breaks resumption
for servers that don't negotiate ALPN at all (legitimate cold-handshake
case), as well as for any persisted ticket predating the
negotiatedAlpn cache field. Treat absent cached ALPN as "no binding to
honour" and allow resumption — the RFC 9001 §4.6.1 restriction is
about ALPN MISMATCH, not absence. The post-EE rejected0Rtt check
mirrors the same null-tolerant shape.

P2 scratch threading: the previous commit added aeadNonceInto but
didn't thread a persistent scratch through the call sites, so the
hot path still allocated a fresh 12-byte nonce per packet. Add
PacketProtection.nonceScratch (sized to iv.size, single-direction so
single-threaded), thread it through Short/LongHeaderPacket build +
parseAndDecrypt as an optional `nonceScratch: ByteArray? = null`
parameter, and pass `proto.nonceScratch` from the four production call
sites in QuicConnectionWriter / QuicConnectionParser. Tests that
construct packets directly are unchanged (default null = allocate
fresh).

All quic JVM unit tests pass; spotless applied.

https://claude.ai/code/session_01EBHtGLy5o7FUR5qfcpHUUx
2026-05-13 19:19:05 +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
Claude fbacef1f2a fix(quic): address #2861 code-review findings
Implements the 10 actionable items from the QUIC code review in issue #2861;
the lone P4 item (encrypt-under-streamsLock) is already tracked as deferred
phase 2 work in quic/plans/2026-05-08-lock-split-design.md and is unchanged.

Android-only correctness (would silently break on API 26–32):
- A1 JdkCertificateValidator: wrap Signature.getInstance("Ed25519") in
  try/catch and surface NoSuchAlgorithmException as a clean
  QuicCodecException so an Ed25519 leaf cert no longer crashes the
  TLS read loop on pre-API-33 Android.
- A2 TlsRunningSha256 (jvmAndroid): keep a parallel byte accumulator and
  fall back to one-shot SHA-256 on the rare API-26–28 Conscrypt builds
  whose OpenSSLMessageDigestJDK throws CloneNotSupportedException from
  MessageDigest.clone(). Latches the fallback after first failure so we
  don't pay the JCA throw per snapshot.

Hot-path performance:
- P1 HeaderProtection: add maskAt(hpKey, src, srcOffset) + extend
  AesOneBlockEncrypt with encryptInto(key, src, srcOffset, dst, dstOffset)
  so the per-packet HP path no longer allocates a 16-byte sample slice
  AND no longer allocates a 16-byte JCA Cipher output (the jvmAndroid
  impl uses Cipher.doFinal's range overload). Updated 5 call sites in
  ShortHeaderPacket and LongHeaderPacket.
- P2 aeadNonce: add aeadNonceInto(staticIv, packetNumber, dst) so call
  sites with a persistent 12-byte scratch can build the nonce without
  per-packet allocation; aeadNonce keeps its existing shape via the new
  helper. Threading the scratch through Short/LongHeaderPacket and the
  writer is deferred (similar shape to the documented P4 phase 2 work).
- P3 QuicConnectionParser: decode the frame list once per inbound packet,
  feed both qlog (frameNamesFor) and dispatch from the single decode.
  Pre-fix every qlog-attached packet ran decodeFrames twice.
- P5 QuicConnectionWriter: iterate pendingMaxStreamData /
  pendingNewConnectionId directly instead of allocating
  entries.toList() per drain.

Protocol / security:
- S1 QuicConnectionParser: cap MAX_STREAMS at 2^60 (RFC 9000 §19.11);
  a peer sending a larger value now triggers STREAM_LIMIT_ERROR close
  rather than overflowing the local nextLocalBidi/UniIndex counters.
- S2 QuicConnection.effectiveResumption: drop the cached session ticket
  when (now - issuedAt) ≥ min(ticketLifetimeSec, 7 days) per RFC 8446
  §4.6.1; expired tickets would otherwise silently fail server-side and
  lose any 0-RTT bytes.
- S3 QuicConnection: refuse to offer 0-RTT when our current alpnList
  doesn't include the resumed session's negotiated ALPN, and treat
  0-RTT as rejected on EE if the new ALPN differs from the cached one
  (RFC 9001 §4.6.1).
- S4 TlsExtension.encodeSignatureAlgorithms: drop rsa_pkcs1_sha256.
  The validator already rejects it in CertificateVerify per RFC 8446
  §4.2.3 — advertising it lied about what we accept.

All quic JVM unit tests pass; spotless applied.

https://claude.ai/code/session_01EBHtGLy5o7FUR5qfcpHUUx
2026-05-13 18:55:52 +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
David Kaspar 15dbde8cc3 Merge pull request #2871 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 17:22:12 +02:00
Crowdin Bot a119da177f New Crowdin translations by GitHub Action 2026-05-13 15:17:11 +00:00
Vitor Pamplona f6adc760bb Merge pull request #2870 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 11:15:17 -04: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
Vitor Pamplona 881e3a9b38 Merge pull request #2869 from davotoula/fix/1180-translation-image-url
Preserve image URLs in CJK translations (#1180)
2026-05-13 10:41:40 -04: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
David Kaspar f288f2d921 Merge pull request #2865 from davotoula/docs/ai-contrib-additions
contributing-with-ai: add 8 rules surfaced by recent feature PR
2026-05-13 11:18:08 +02:00
David Kaspar a1b27792f9 Merge pull request #2864 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 11:16:35 +02:00
davotoula 286344c254 docs(contributing-with-ai): add 8 rules surfaced by recent feature PR
Pulled from a retrospective on a feature PR that exercised every rule
already in the doc, and surfaced eight gaps where the doc was silent.

- Protocol-introducing changes (new section): require an explicit
  cross-client compatibility section, on-relay wire-format verification
  via nak, and a NIP spec citation when claiming "NIP-X allows this".
  These three together prevent the "syncs across devices" handwave
  from masking real interop questions.
- Release-minified build subsection in "Build and install both
  flavours": call out the existing benchmark build type
  (com.vitorpamplona.amethyst.benchmark, profileable=true, R8-active)
  as the cheap rig for catching ViewModel-factory / expect-actual /
  serialization R8-stripping bugs before the reviewer does.
- Strip diagnostic Log.d before commit: explicit policy. Lambda Log
  is required for committed logs; temporary debug Log.d gets removed.
- Regression-test-plan worked example: shows the required
  ### Feature test plan / ### Regression test plan structure with a
  real-shaped example, so contributors don't ship the wrong subheadings.
- Multi-device sync touch-point: added to the regression sweep
  checklist for features that publish per-account state to relays.
- R8-minified-build touch-point: added to the same checklist for
  reflection-heavy code paths.
- "When on-device QA finds bugs in your own diff" subsection in
  "Code review pass": promote the pattern of landing manual-QA
  fixes as a separate commit with root-cause prose, not folding
  silently into the feature commit.
- Working-notes convention in "Everything else": clarify
  docs/plans/ is tracked, docs/brainstorms/ + docs/superpowers/
  are gitignored. Saves AI tools from trying to commit scratch work.
2026-05-13 11:15:31 +02:00
Crowdin Bot fb74d5e0f1 New Crowdin translations by GitHub Action 2026-05-13 08:55:53 +00:00
David Kaspar f272f24afa Merge pull request #2860 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-13 10:54:25 +02: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