Commit Graph

13480 Commits

Author SHA1 Message Date
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
davotoula 9e88ff03db refactor(sonar): merge nested if statements
Sonar kotlin:S1066 — collapse 'if (A) { if (B) { body } }' into 'if (A && B) { body }'
across 16 files. Kotlin's && short-circuits identically to the nested form, so this
is a behaviour-preserving change.
2026-05-13 10:11:50 +02:00
davotoula e21794e42a Manual testing fixes:
- quartz: Event.threadRootIdOrSelf() now falls back to markedReply()?.eventId
when both markedRoot() and unmarkedRoot() are absent
- amethyst: After upstream PR #2855 split the notifications feed into
notificationsFollowing and notificationsEveryone, the hiddenUsers.flow
collector still only invalidated the original notifications feed. Extends
it to invalidate all three.
- amethyst: CardFeedContentState.refreshSuspended() takes an additive-only
path when lastNotes is populated — it only adds cards for new admissions,
never removing cards for notes that no longer pass the filter. Re-muting
mid-session correctly rejected muted-thread reactions in the filter, but
the existing cards stayed in the UI as "Show Anyway" placeholders. Calls
clear() before invalidateData() on each notification feed so the refresh
hits the full-rebuild branch and removals propagate.
2026-05-13 09:06:53 +02:00
davotoula ff780bcb54 Code review:
- consolidate thread-root resolution
- consolidate mute-state writes
2026-05-13 09:06:53 +02:00
davotoula a99488779a Amethyst:
- drop muted-thread reactions/zaps from notifications feed
- SecurityFiltersScreen lists muted threads with unmute action
- MutedThreadsFeedFilter for settings screen
- add Mute thread entry to long-press dropdown
- add Mute thread entry to quick-action sheet
- add mute-thread string resources
- AccountViewModel.muteThread/unmuteThread/isThreadMutedFor
- drop muted-thread events before Android push dispatch
- FilterByListParams drops muted-thread notes
- Account.isAcceptable drops muted-thread notes
- Account.muteThread/unmuteThread + resolveThreadRoot + isThreadMuted
- MuteListDecryptionCache exposes mutedThreadIdSet helper
- MuteListState supports hideThread/showThread
- HiddenUsersState exposes muted-thread root ids
2026-05-13 09:06:53 +02:00
davotoula bac3710deb Commons:
- Note.isHiddenFor drops notes in muted threads
- LiveHiddenUsers carries mutedThreads + isThreadMuted predicate
2026-05-13 09:06:53 +02:00
David bbb4e39465 Quartz:
- add mutedThreads/mutedThreadIdSet accessors
- MuteTag sealed interface recognizes EventTag
- add EventTag for NIP-51 mute-list e tags
- MuteListEvent round-trip and legacy-tag migration coverage
2026-05-13 09:06:53 +02:00
Crowdin Bot 5bc319e9b9 New Crowdin translations by GitHub Action 2026-05-13 02:23:02 +00:00
Vitor Pamplona 1d503dc6ac Adds android.util.Log mock for quic tests
Mirrors the quartz commonTest stub so :quic:testAndroidHostTest no longer
throws RuntimeException("Stub!") through PlatformLog.android on the
MAX_STREAMS_UNI emission path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:19:48 -04:00
Vitor Pamplona 6ab077361f updates dependencies 2026-05-12 22:14:06 -04:00
Vitor Pamplona 65c72605a0 function names cannot have @ in iOS 2026-05-12 22:07:23 -04:00
Vitor Pamplona 5c39e3c85b Removes more warnings 2026-05-12 22:05:38 -04:00
Vitor Pamplona c51c5f665b fix(quartz): make commonMain compile for Kotlin/Native (iOS)
@Volatile resolves via kotlin.jvm on JVM but needs an explicit
kotlin.concurrent.Volatile import on Native; Dispatchers.IO is internal
on Native, so the in-process WebSocket switches to Dispatchers.Default
(no blocking I/O on that path); and Native's LinkedHashMap is final
without removeEldestEntry, so Nip98AuthVerifier's replay cache now caps
itself with an explicit insertion-order eviction loop (access-order was
unused — the cache is write-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:03:19 -04:00
Vitor Pamplona 2eec53472a Adds a log mock 2026-05-12 19:40:28 -04:00
Vitor Pamplona da32b33684 fixes warnings 2026-05-12 19:32:11 -04:00
Vitor Pamplona 3751d6dc28 Fixes warnings 2026-05-12 19:27:12 -04:00
Vitor Pamplona 349f7a7989 fix(quartz): make IngestQueue writer-start KMP-safe via AtomicBoolean
Replaces JVM-only @Volatile + synchronized double-checked locking with
kotlin.concurrent.atomics.AtomicBoolean.compareAndSet so the file builds
on all commonMain targets, mirroring the pattern in BasicRelayClient.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:21:14 -04:00
Vitor Pamplona 00c5c32041 merge 2026-05-12 19:05:59 -04:00
Vitor Pamplona 14342e12c2 refactor(geode): simplify RelayEngine + trim StaticConfig comments
RelayEngine (224 → 185 lines):
  - Extract BanStore <-> RuntimeConfigData mapping helpers
    (seedInto, snapshotOf) into geode.config — bidirectional
    conversion now has names instead of being inline boilerplate
    repeated between the init block and snapshot().
  - Collapse the standalone init block into a .apply on the
    banStore declaration. Method reference ::snapshot for the
    onMutation hook.
  - Drop the effectiveAtBoot field's 12-line KDoc (rename to `boot`
    locally, terse). Trim verbose property KDocs to the load-bearing
    facts (why-not-what).

StaticConfig (255 → 182 lines):
  - Drop resolveInfo's unused `advertisedUrl` parameter and the
    workaround comment that justified keeping it for "future
    fields". YAGNI — add back when actually needed. Two callers
    updated.
  - Trim trivial KDocs (fromToml/fromFile, field names that
    document themselves like require_auth, reject_future_seconds,
    supported_nips, database.file).
  - Collapse NetworkSection's three thread-pool KDocs (~16 lines)
    into a single section-level KDoc.
  - Compact parallel_verify, AdminSection, AdminSection.state_file,
    and advertisedUrl companion KDocs while keeping the
    load-bearing facts (invariants, strfry interop strings,
    surprising defaults).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:01:36 -04:00
Vitor Pamplona 72e7f37b61 refactor(geode): move Nip86Server (+ adminPubkeys) into RelayEngine
KtorRelay was carrying state that wasn't HTTP-specific: the
Nip86Server, its InfoHolder adapter, and the adminPubkeys allow-list.
Push them into RelayEngine — it already owns the info doc, ban store,
and event store the Nip86Server consults; the allow-list is a
relay-level "who is admin?" decision, not a transport-level one.

RelayEngine gains adminPubkeys: Set<HexKey> = emptySet() and exposes
nip86Server as a public property. Future non-HTTP admin transports
(in-process tools, hypothetical NIP-86-over-WS) read it directly
without re-deriving the wiring.

KtorRelay shrinks to mostly Ktor structures — routing block, engine
config, lifecycle. It just builds the Nip86HttpHandler around
relay.nip86Server.

Main.kt and Nip86EndToEndTest pass adminPubkeys to RelayEngine
instead of KtorRelay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:44:50 -04:00
davotoula b20377cf0e i18n: translate nest-room moderation + split-notifications strings into cs, de, pt-BR, sv 2026-05-13 00:34:42 +02:00
Vitor Pamplona 445fbf8cda refactor(quartz, geode): derive NIP-86 publicUrl from relay.url; drop isEnabled branch
Two simplifications:

1. Derive admin URL from RelayEngine.url. NIP-86 spec mandates that
   the admin endpoint is "the same URI as ws(s)://, called via
   http(s)://" — so KtorRelay derives the NIP-98 binding URL by
   calling relay.url.toHttp() instead of accepting publicUrl as a
   separate config. Single source of truth (info.relay_url),
   accidental misconfiguration impossible, no Host-header fallback.
   StaticConfig.AdminSection.public_url is gone.

2. Uniform code path for admin-enabled vs disabled. Empty allow-list
   isn't a special case anywhere: Nip86Server.isAuthorized returns
   false for everyone, dispatch rejects, Nip86HttpHandler returns
   NotAdmin → 403. KtorRelay always assembles the Nip86HttpRoute
   and registers the POST endpoint; Nip86Server.isEnabled() and the
   "is admin on?" branches in the handler and route are deleted.

Nip86EndToEndTest's admin-disabled case is now a behavioral test:
sign a valid token, expect 403 NotAdmin (was 405 with the no-route
variant, was 403 with the fake-disabled-route variant).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:19:24 -04:00
David Kaspar 4e199987e9 Merge pull request #2859 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-12 23:57:42 +02:00
Vitor Pamplona a53b3339b3 refactor(quartz): canonical NIP-86 HTTP flow in Nip86HttpHandler
Move the NIP-86-over-HTTP orchestration (NIP-98 verify → admin
allow-list gate → parse → dispatch → serialize) into quartz so any
relay implementation that wants the standard transport can plug its
HTTP framework in without re-deriving the sequence — and without
accidentally skipping NIP-98 verification or the admin check.

  • Nip86Server (quartz) gains allowList + isEnabled() + isAuthorized();
    dispatch becomes dispatch(pubkey, req) and enforces the allow-list
    itself, so in-process callers can't bypass it either.
  • Nip86HttpHandler (new, quartz) takes only primitives (authHeader,
    url, body) and returns a sealed Response — Disabled, PayloadTooLarge,
    MissingAuth, BadAuth, NotAdmin, BadRequest, Ok(pubkey, req, resp,
    json). Constants for the 1 MiB body cap, application/nostr+json+rpc
    content type, and WWW-Authenticate scheme live here.
  • Nip86HttpRoute (geode) shrinks to a Ktor adapter: bounded read,
    extract URL/auth, call handler, map Response → Ktor status codes.
    Audit logging stays in geode — not a quartz concern.

External relay authors: import quartz, build a Nip86HttpHandler with
their Nip86Server + Nip98AuthVerifier, and write a ~30-line adapter
for their framework. The full security-critical flow is in the box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:38:40 -04:00
Vitor Pamplona e4e9ae7043 refactor(quartz): NIP-86 bans purge matching events via onBan(filter) hook
Nip86Server used to take an optional IEventStore and call store.delete()
only on banevent — banpubkey and disallowkind would update the BanStore
but leave existing matching events served by REQ. That's inconsistent
(an operator who bans a spam pubkey expects the spam to be gone) and
a real safety issue (banning a CSAM event wouldn't actually remove
existing copies if the store was null).

Replace the IEventStore param with onBan: suspend (Filter) -> Unit. The
dispatcher fires it after each ban method with the Filter that selects
the now-banned events:
  banpubkey   → Filter(authors = [pk])
  banevent    → Filter(ids = [id])
  disallowkind→ Filter(kinds = [k])

KtorRelay wires onBan = { f -> relay.store.delete(f) }, so the existing
SQLite delete-by-filter path handles every shape without the caller
having to know which field to populate. The default no-op keeps the
unit test for the dispatcher dependency-free.

withInt is now suspend so disallowKind's handler can call the suspending
onBan inline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:55:38 -04:00
Vitor Pamplona a010b37d6c refactor(geode): inject RuntimeConfig into RelayEngine, mirroring EventStore
RelayEngine used to take three params that were really RuntimeConfig
construction details — info: RelayInfo, stateFile: File?,
seedAuthorization: AuthorizationSeed — and assembled the RuntimeConfig
internally. Replace that with a single runtimeConfig: RuntimeConfig
param (with an in-memory default), matching how store: IEventStore is
already passed in. Main.kt builds the RuntimeConfig from StaticConfig
at the assembly layer where the TOML→runtime mapping is most explicit.

AuthorizationSeed is dropped — it only existed to shuttle data into
the now-removed seedAuthorization param. Callers build RuntimeConfigData
directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:40:14 -04:00
Vitor Pamplona f4bcf38caf refactor(geode): drop unused KtorRelay knobs (maxFrameBytes, maxAdminBodyBytes)
Ktor's WebSocket layer is unbounded by default, so [limits].max_ws_frame_bytes
was a strfry-parity tuning knob nobody actually used — drop the whole
LimitsSection from StaticConfig and the matching constructor param from
KtorRelay. The negentropy-large-corpus plan note is updated accordingly.

maxAdminBodyBytes is a real defense-in-depth cap (NIP-98 forces us to
read the body for the payload sha256 before we can authenticate, so an
unbounded read is a pre-auth DoS vector). Keep the cap, but stop
exposing it as an operator knob — it was never plumbed through to
StaticConfig and 1 MiB is ~1000× any plausible NIP-86 RPC payload.
Hardcoded at the Nip86HttpRoute construction site with the rationale
inline. Nip86HttpRoute.maxBodyBytes stays so tests can drive the 413
boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:15 -04:00
Vitor Pamplona 47fdd8638e refactor(geode): extract NIP-11 GET into Nip11HttpRoute
KtorRelay was resolving NIP-11 inline in the routing block while NIP-86
already had a dedicated Nip86HttpRoute with a handle(call) entry point.
Lift NIP-11 to the same shape so both endpoints look symmetric and the
routing block is just dispatch. The `liveJson` callback (rather than a
snapshot string) keeps NIP-86 changerelay* admin mutations visible on
the next GET without re-wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:58:05 -04:00
Vitor Pamplona b0ad540453 refactor(geode): split config into StaticConfig + RuntimeConfig with seeding
Rename RelayConfig → StaticConfig and the persistence/RelayStateStore →
config/RuntimeConfig so the two configuration tiers — boot-time TOML
versus operator-mutable runtime state — are obvious from the class
names. The `persistence/` package is gone; everything related to config
lives in `config/`.

For overlapping fields (NIP-11 info doc, pubkey / kind allow-deny
lists), StaticConfig now seeds RuntimeConfig on first boot via the new
RuntimeConfig(seed = …) constructor and an AuthorizationSeed type. The
runtime file wins from then on: later edits to [authorization] in the
TOML are ignored once an admin RPC has written the snapshot. As a
consequence, KindAllowDenyPolicy and PubkeyAllowDenyPolicy are no
longer stacked in geode's policy chain — BanListPolicy already reads
the same lists from the BanStore, and double-stacking would silently
diverge after the first admin mutation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:51:19 -04:00