Commit Graph
169 Commits
Author SHA1 Message Date
vitorpamplonaandgithub-actions[bot] 757f348a46 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-11 15:04:41 +00:00
davotoulaandgithub-actions[bot] 2edb22783c chore: sync Crowdin translations and seed translator npub placeholders 2026-07-11 14:32:20 +00:00
davotoulaandgithub-actions[bot] cc275ce62d chore: sync Crowdin translations and seed translator npub placeholders 2026-07-11 10:47:38 +00:00
vitorpamplonaandgithub-actions[bot] 835f1e5e85 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-11 03:16:54 +00:00
vitorpamplonaandgithub-actions[bot] 085b4d7dc0 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-10 17:06:25 +00:00
nrobi144andClaude Opus 4.8 fe96311d93 test: record 2026-07-09 live run results + fix macOS prefs path in sheet
- Correct the AUTH-approval persistence check: this JVM uses
  MacOSXPreferences (~/Library/Preferences/com.vitorpamplona.amethyst.plist),
  not ~/.java/.userPrefs. Updated T3.b/c/d to read it via plutil.
- Record session results (T1,T2,T3a,T3c,T6,T6b,T8,T12 pass) and the three
  bugs found+fixed during the run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:43:03 +03:00
nrobi144andClaude Opus 4.8 abe4668fd8 test: add T6b strict kind:10050 non-leak scenario; refresh sheet header
Covers the review fix — recipient with NIP-65 read relays but no
kind:10050 must be treated as unreachable, not routed to the read relays.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 07:56:42 +03:00
nrobi144andClaude Opus 4.8 9e707da2b1 fix(desktop): use strict kind:10050 in DM inbox resolver LocalCache fast-path
The indexer fan-out already used lists.dmInbox (strict), but the
LocalCache fast-path in DmInboxRelayResolver.resolve() — and the
no-resolver fallback in DesktopIAccount — went through the lenient
User.dmInboxRelays(), which falls back to NIP-65 read relays (kind:10002)
when the recipient has no kind:10050.

Because that fast-path returns first and short-circuits the strict
indexer lookup, a recipient with NIP-65 read relays but no published
DM-inbox would get gift wraps published to relays they never designated
for DMs — re-introducing the metadata leak (recipient pubkey + send
timing) the P0 fix was meant to close. LocalCache commonly holds
kind:10002 but not kind:10050, so this path fired often.

Switch both LocalCache lookups to dmInboxRelaysStrict().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 07:45:34 +03:00
nrobi144 33417d8a58 test: unpack testing sheet into linear playbook
Rewrites the manual testing sheet so every step is an executable action
or a specific observation:

- Numbered steps within each test — no jumping between reference
  sections. "Click X", "run Y in a terminal", "watch for Z".
- Each observation records YES/NO/SKIPPED for the sign-off matrix.
- Setup section spells out the wipe-preferences command and the
  post-restart smoke check.
- T3 broken into T3.a/b/c/d for each button and its persistence
  check separately (previously bundled T3.1–T3.10 was too dense).
- T6/T7 include exact tcpdump/tshark commands for the security
  observations that require packet capture.
- T13 shortened to a sanity re-check (already verified).
- Sign-off table lists every test with a checkbox.

Purpose: give the tester a self-contained document they can follow
top to bottom in ~40 min without cross-referencing other sheets.
2026-07-09 07:33:43 +03:00
nrobi144 0fc9b8a87d test: consolidate manual testing sheet, add T13 pre-send fix coverage
Updates the manual walkthrough to reflect the full state of the branch
after rebase + banner padding fix + pre-send validation fix:

- Marks what's already verified (T1 startup wiring, T5 lifecycle,
  T6 P0 security via upstream UI, T13 pre-send fix).
- Adds T13 covering the alignment + resolver-probe fix from
  a139c0fb17 with three sub-scenarios (strict alignment, resolver
  unblock, and the real 2-user bootstrap flow).
- Documents the known pre-existing issues surfaced during testing
  (RelayLatencyTracker CME, CompressionQuality stale-daemon) so
  they don't get filed as regressions of ours.
- Adds a summary table pointing to the automated sheet for the
  Phase A greens.
- Explicit out-of-scope list mirroring the deepening synthesis.

No code changes — documentation only.
2026-07-09 07:33:43 +03:00
nrobi144 68d1276b1a test: testing sheet for feat/desktop-dm-reliability
Captures every verification step for the 16-commit branch:
  §A  automated — compile + unit tests + spotless + package + branch
      integrity. All passing as of 2026-06-12.
  §B  manual desktop — AUTH end-to-end (tier-1 + tier-2), banner UX,
      NIP-17 send security, indexer fan-out, group DM rumor coherence,
      relay-hint placement, outbox AUTH carve-out, bunker concurrency
      cap, SigningOpState.Progress.
  §C  Android sanity — commons inheritance check.
  §D  security audit — code/git inspection (D1 all passing), threat
      walkthrough (D2).
  §E  sign-off matrix.

Includes a known-gap callout for D2.2 (logout does NOT clear AUTH
approvals — by design; account-delete is the trigger, follow-up
verification needed) and an out-of-scope list mirroring the
deepening synthesis (retry queue, per-message bubble UI, NIP-46
batch RPC, Android UI parity).
2026-07-09 07:33:42 +03:00
vitorpamplonaandgithub-actions[bot] 61507acdd7 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-08 21:32:24 +00:00
Vitor PamplonaandGitHub 0ff48cfbe0 Merge pull request #3483 from nrobi144/feat/wot-shared-index-relays
feat(desktop): Web-of-Trust score badges + shared index relays + amy wot verbs
2026-07-08 12:53:08 -04:00
Vitor PamplonaandGitHub 50ce22e49e Merge pull request #3495 from nrobi144/feat/desktop-wallet-privacy-lock
feat(desktop): apply the privacy lock to the Wallet column
2026-07-08 12:52:54 -04:00
nrobi144 1e076c5cc2 feat(desktop): apply privacy lock to the Wallet column
Extends the messaging privacy lock to the Wallet deck column via the
same master `lockEnabled` flag (single toggle, single password) with
per-scope lock state so each route re-locks independently.

commons/ui/privacylock/
  LockScreen.kt        Shared internal composable (scope + copy)
  WalletLockGate.kt    Mirrors MessagesLockGate for scope=Wallet
  MessagesLockGate.kt  Shrunk to a 20-LOC wrapper delegating to LockScreen

desktopApp/security/
  DesktopLockScreen.kt         Shared password-input surface with optional
                               "No password set" deep-link (plan Q5).
  DesktopMessagesLockGate.kt   Now delegates to DesktopLockScreen
  DesktopWalletLockGate.kt     New; deep-links to Settings via
                               onNavigateToRelays when no password is set
  WalletFirstRunBanner.kt      Mirrors MessagesFirstRunBanner; both read
                               the single firstRunCardSeen flag (dismiss
                               once = dismissed everywhere)
  MessagesFirstRunBanner.kt    Copy updated: "Lock Messages and Wallet?"
  PrivacyLockBlurModifier.kt   Modifier.privacyLockBlurWhenUnfocused()
                               reads LocalWindowInfo.isWindowFocused;
                               applied to text nodes only (balance,
                               generated-invoice amount, QR code) — cards
                               and layout stay crisp (plan Q4).

desktopApp/ui/
  wallet/WalletColumnScreen.kt Inserts WalletFirstRunBanner at top;
                               wraps sensitive text with blur modifier.
  deck/DeckColumnContainer.kt  Wraps Wallet branch with
                               DesktopWalletLockGate; passes
                               onNavigateToRelays so the "No password"
                               branch deep-links to Settings.
  settings/PrivacyLockSettingsScreen.kt
                               Master-lock copy: "Enable privacy lock"
                               header; body mentions Messages AND Wallet
                               columns; auto-lock + caveat cards updated
                               to reference both routes.

Testing sheet: docs/plans/2026-07-07-wallet-lock-manual-testing.md
  12 manual scenarios covering cross-scope lockout, blur-on-unfocus,
  password-clear cascade, deep-link to Settings, and first-run banner
  parity across the two routes.

All existing PrivacyLockStateTest cases green + the 3 Wallet-reuse
tests from the previous commit. amethyst + desktopApp compile clean.
2026-07-07 13:33:32 +03:00
nrobi144 d0daf786b1 feat(commons): scope-parameterise PrivacyLockState for multi-route lock reuse
Genericises the messaging privacy-lock state holder so a single master
`lockEnabled` flag can drive multiple gated routes independently:

- `LockScope { Messages, Wallet }` enum added.
- `MessagesLockState` → `PrivacyLockState(scope, settings, coroutineScope)`.
  Each scope keeps its own StateFlow<LockState> + idle-timer Job; both
  scopes share the same `PrivacyLockSettings` so failed-attempt counters
  and lockout schedule stay device-global (brute-force protection).
- `LocalMessagesLockState` (single instance) → `LocalPrivacyLockState`
  (Map<LockScope, PrivacyLockState>) + `lockStateFor(scope)` accessor.
- `redactionLevel` → `dmRedactionLevel` (Kotlin-side rename; persisted
  prefs key `redaction_level_ordinal` unchanged).
- `setPasswordHashed(null)` cascades to `setLockEnabled(false)` so a
  master lock cannot stay armed without a credential to verify against.

MessagesLockGate, DesktopMessagesLockGate, MessagesFirstRunBanner,
SetPasswordDialog, and RedactionCard now read `lockStateFor(Messages)`
— behaviour-preserving. Ships 3 new PrivacyLockStateTest cases:
independent per-scope state, shared failed-attempt counter, and the
password-clear cascade.

Plan: docs/plans/2026-07-07-feat-wallet-privacy-lock-reuse-plan.md
2026-07-07 13:18:56 +03:00
nrobi144andClaude Opus 4.7 6361c54a4e fix(desktop): sidebar nav replaces detail overlay instead of hiding behind it
On Desktop, tapping a sidebar nav item while a detail screen (profile,
thread, article, editor) was open only mutated the sidebar destination.
The opaque `AnimatedContent` overlay driven by `ColumnNavigationState`
kept covering the (already-swapped) root content until the user hit
Back, creating the impression that the click did nothing.

Fix: emit a `clearOverlaySignal` from `SinglePaneState.navigate` and
`DeckState.focusExistingColumn`. Each layout collects the signal in a
`LaunchedEffect` and calls `navState.clear()`, draining any pending
detail stack so the tapped destination is what the user actually sees.

- SINGLE_PANE: one signal (Unit), one layout-local `navState`.
- DECK: signal payload is the column id; each `DeckColumnContainer`
  filters on `column.id`, so only the focused column's detail clears —
  other columns' navigation stacks are preserved.
- Same-item taps also clear (signal fires unconditionally, unlike a
  StateFlow value comparison).
- `onOpenSettings` uses the same navigate / focusExistingColumn paths
  and inherits the fix automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-06 16:08:06 +03:00
nrobi144 fe22de0817 feat: shared index relays across Desktop and amy + settings UI
Unifies the "index relays" set (used for kind 0 profile metadata and
kind 3 follow list REQs) across the Desktop app and the `amy` CLI so
they always compute WoT scores against the same data source, and adds
a user-configurable settings section for the list.

Before this change:
- Desktop hard-coded `DefaultRelays.RELAYS` at coordinator
  construction; users could not override.
- `amy wot sync` used `outboxRelays().ifEmpty { inboxRelays() }` —
  NIP-65 write / DM inbox relays, which are semantically different
  from index relays. `amy wot get` after `amy wot sync` could return a
  different score than the Desktop UI would compute.

New `PreferencesIndexRelays` (commons/jvmMain) is a tiny class backed
by `java.util.prefs.Preferences.userRoot().node("com/vitorpamplona/amethyst/relays/index")` —
the same JVM-user-scoped shared-node trick `PreferencesHashtagSpamSettings`
already relies on. Both Desktop and amy running as the same OS user
observe the same value with zero extra plumbing. App-global (not
per-account); users typically have one preferred index-relay set
regardless of which account is logged in.

Behaviour changes for users who never open the settings UI: none.
`DEFAULT_INDEX_RELAYS` is byte-for-byte identical to the four URLs in
`DefaultRelays.RELAYS`.

Wiring:
- `DesktopRelayCategories` gains a straight-through `indexRelays`
  StateFlow (no combine — index relays are a curated user choice, not
  a NIP-65-derived set) plus `setIndexRelays(new)`.
- `Main.kt` instantiates `PreferencesIndexRelays` at App() root and
  passes it into both the subscriptions-coordinator constructor and
  `DesktopRelayCategories`. Coordinator snapshots the effective set
  at construction — changes take effect on next relaunch (documented
  in the settings section explainer).
- `Context.indexRelays()` reads the same preferences node so
  `WotCommand.sync` produces identical relay batches to Desktop.
- New `IndexRelaysSection` composable in
  `desktopApp/.../ui/settings/` — list + per-row remove + add-row
  with URL normalisation. Deletion of all entries falls back to
  defaults (delete-all is the reset — no separate "Reset" button).
  Placed between the Local Relay and Content Filters sections of the
  Relays settings screen.

Tests:
- `PreferencesIndexRelaysTest` — defaults fallback, round-trip
  persistence, blank-token skipping, non-empty defaults guardrail.
- Full existing test suites remain green.

Companion PR (search-result badges) landed on `feat/wot-search-badges`
and is this branch's parent. Both remain stacked on the WoT feature
branch pending upstream review.

Plan: docs/plans/2026-07-01-feat-wot-followups-search-badges-and-index-relays-plan.md
2026-07-06 09:31:02 +03:00
vitorpamplonaandgithub-actions[bot] 58a72c02fa chore: sync Crowdin translations and seed translator npub placeholders 2026-07-03 18:39:52 +00:00
davotoulaandgithub-actions[bot] 5e6405e3c9 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-03 18:08:00 +00:00
vitorpamplonaandgithub-actions[bot] 9eb447de38 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-03 14:39:19 +00:00
nrobi144 9284e12e86 feat(desktop): Web-of-Trust score badges + amy wot verbs
Adds a friends-of-friends trust score on every user avatar in Desktop
feeds, threads, profile headers, and repost overlays. For pubkey X the
score is the count of accounts in the active user's follow set who also
follow X — Gossip / Snort convention. v1 is display-only; no threshold
filtering.

Data flow
- `commons/wot/WoTService` — sparse `SnapshotStateMap<HexKey, Int>` +
  reverse index + per-follower snapshot for diff-based updates.
  Single-writer coroutine (Channel<Op> → `Snapshot.withMutableSnapshot`)
  serializes all mutations. Cap at 5000 follows/event blocks DoS via
  hostile kind-3s. Guardrail at 2000 follows/account skips WoT for
  mega-accounts.
- `DesktopIAccount.wotService` — per-account instance, matches
  `Kind3FollowListState` / `BookmarkListState` conventions.
- `Main.kt` binds `localCache.accountPubkey`, collects
  `localCache.contactListEvents` → `applyKind3`, collects
  `localCache.followedUsers` → `onFollowSetChange` +
  `subscriptionsCoordinator.loadKind3Batched(...)` with
  `onEose = markReadyOnce`. 2 s fallback timeout guarantees badge
  visibility even if index relays never EOSE.
- `FeedMetadataCoordinator.loadKind3Batched(pubkeys, onEose)` — chunks
  authors into ≤100 per Filter within one subscription. Matches
  nostr-rs-relay defaults.

UI
- `commons/ui/components/UserAvatar` gets an optional
  `badge: @Composable BoxScope.() -> Unit`. Android call sites pass
  null (no compile-time coupling to Desktop-only tooltip APIs).
- `desktopApp/.../ui/note/WoTBadge` — Material3 `TooltipBox` +
  `PlainTooltip` (multiplatform-ready, keyboard/screen-reader a11y).
  `rememberTooltipState(isPersistent = true)` fixes the
  vanish-too-fast desktop default.
- `desktopApp/.../ui/note/WoTBadgedAvatar` — drop-in replacement for
  `UserAvatar` that overlays the badge when
  `LocalWoTService != null && LocalWoTReady && pubkey !in LocalSpamExemptKeys`.
  Score read is a plain `service.scores[userHex] ?: 0` — snapshot
  system tracks per-key, so avatars only recompose when their own
  score changes.
- Call-site migration at 4 v1 surfaces: NoteCard header (covers feed /
  thread / bookmarks / search / QuotedNoteEmbed via NoteCard),
  FeedNoteCard repost header (2 avatars), UserProfileScreen header
  (2 sizes).

Amy verbs
- `amy wot get <pubkey|npub> [--json]` — hydrates a WoTService from the
  local FsEventStore, prints score for target pubkey.
- `amy wot list [--threshold N] [--limit K] [--json]` — sorted score
  list.
- `amy wot sync [--timeout N]` — batch-fetches kind-3 for the active
  follow set from outbox/inbox relays, persists to the event store.

Tests + docs
- 14 unit tests: `WoTServiceTest` covers happy path, sparse map,
  self/follower exclusion, kind-3 churn diff, guardrail, event cap,
  ready gate, clear.
- Manual testing sheet with 17 scenarios at
  `desktopApp/plans/2026-07-01-wot-score-manual-testing-sheet.md`.
- Plan at `docs/plans/2026-07-01-feat-desktop-wot-score-plan.md`.

Prerequisite `DesktopLocalCache.consumeContactList` scoping fix landed
as a separate commit.
2026-07-02 18:03:47 +03:00
vitorpamplonaandgithub-actions[bot] 5c40187ba0 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-02 11:05:32 +00:00
nrobi144 33bb81dddb Merge remote-tracking branch 'upstream/main' into feat/desktop-privacy-lock
# Conflicts:
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt
2026-07-02 07:20:44 +03:00
vitorpamplonaandgithub-actions[bot] 73c9e2086a chore: sync Crowdin translations and seed translator npub placeholders 2026-07-01 21:19:15 +00:00
Vitor PamplonaandGitHub 3246f13044 Merge pull request #3431 from nrobi144/feat/desktop-hashtag-spam-filter
feat(desktop): hashtag-spam filter with collapse-with-reveal
2026-07-01 07:35:37 -04:00
nrobi144 72c3dff870 feat(privacylock): P0 security hardening — 600k iterations + backoff
Addresses the P0 items in the security review at
docs/plans/2026-07-01-privacy-lock-security-review.md.

## PBKDF2 iterations 100k → 600k (M1) via versioned hash format (M2)

- New PasswordHasher storage format: `v1$saltB64$hashB64` (600k
  iterations, matches OWASP 2023 Password Storage Cheat Sheet for
  PBKDF2-HMAC-SHA256).
- Legacy `saltB64$hashB64` (100k iterations) format still verifies
  correctly — no user gets locked out by the bump.
- `hash()` always produces `v1$…`; users migrate to v1 opportunistically
  when they Change or Set a new password.
- New `PasswordHasher.isLegacyFormat()` helper for callers that want
  to force-migrate on next successful unlock.
- Verify cost goes from ~50ms → ~250ms on a modern laptop — well
  within tolerable UX for a lock users open a handful of times per
  session.

## Exponential backoff on failed unlock (M3)

- `PrivacyLockSettings` gains `failedUnlockAttempts: StateFlow<Int>`
  and `lockedUntilEpochMs: StateFlow<Long?>`, both persisted via
  java.util.prefs so a reboot cannot reset the backoff.
- `MessagesLockState.onFailedUnlockAttempt(nowMs)` implements the
  schedule: no lockout for first 4 fails, then 30s / 60s / 120s /
  300s (capped at 5 min).
- `MessagesLockState.onUnlockSuccess()` transparently clears the
  attempt counter and any active lockout (also called from the
  banner-enable path).
- DesktopLockScreen shows a countdown ("Try again in 27s") in the
  supportingText, disables the password field and Unlock button
  during lockout, ticks every 500ms via a LaunchedEffect.
- RemovePasswordDialog inherits the same protection — Settings can't
  bypass the throttle by disabling the lock.
- 4 new unit tests cover threshold behavior, base trip, doubling +
  cap, reset on success. All 13 tests green.

## Not in this commit

- L1/L2 (String/CharArray memory retention) — out-of-tree fix in
  Compose; accepted per threat model.
- L3 (post-uninstall prefs) — release-notes item.
- M4 (Limitations copy update) — deferred; existing "does not
  protect against filesystem access" line already covers.
2026-07-01 12:06:45 +03:00
nrobi144 0ff4ef1e10 docs(privacylock): manual testing sheet, banner plan, security review
- docs/plans/2026-06-30-privacy-lock-manual-testing.md — 10-path
  manual QA sheet covering setup, lock behavior, timer, deep-link
  race, change/remove password, banner discovery, cross-run
  persistence.
- docs/plans/2026-07-01-feat-messages-first-run-lock-banner-plan.md —
  design doc for the discovery banner shipped in aadbf3601.
- docs/plans/2026-07-01-privacy-lock-security-review.md — honest
  review of PasswordHasher (PBKDF2 100k / 16B salt) + prefs storage.
  Flags 4 medium-severity items (iterations below OWASP 2023 rec;
  no versioned hash format; no UI-layer rate-limiting; unencrypted
  prefs storage) with concrete fixes. All within accepted threat
  model but P0 items are cheap and high-value follow-ups.
2026-07-01 11:35:52 +03:00
nrobi144 c4f647d01a feat(privacylock): foundation — state holder, settings, gate composable
Phase 1 of the messaging privacy lock. Headless cross-platform spine in
commons; no UI wiring yet.

- LockState sealed interface (Disabled / Locked / Unlocked)
- InactivityTimer enum (1m / 5m / 15m / 1h / Never; default 5m)
- DmRedactionLevel enum (Generic / Full)
- PrivacyLockSettings interface (StateFlows + mutators)
- PreferencesPrivacyLockSettings backed by java.util.prefs in jvmAndroid
  (shared by Desktop + Android; node com/vitorpamplona/amethyst/privacylock)
- MessagesLockState — app-global state holder; initial value seeded
  synchronously from prefs to close the deep-link race; LocalMessagesLockState
  CompositionLocal provided at App root
- CredentialPrompter interface + PromptResult enum +
  LocalCredentialPrompter CompositionLocal
- MessagesLockGate composable — synchronous branch select (no
  LaunchedEffect guard); LockScreen with biometric button
- IdleTimerModifier — pointerInput Initial-pass, non-consuming
- 8 unit tests covering cold-start seed, idle expiry, leave-route,
  Never timer, user-interaction reset, settings cascade,
  credential-unavailable disable. All green.
2026-07-01 11:35:51 +03:00
davotoulaandgithub-actions[bot] dde6616451 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-01 08:27:22 +00:00
nrobi144 d0646acf3c Merge upstream/main into feat/desktop-hashtag-spam-filter 2026-07-01 09:53:09 +03:00
nrobi144 7a18e30fc4 feat(desktop): hashtag-spam filter with collapse-with-reveal
Damus-inspired content filter that collapses notes abusing `t` hashtag
tags into a compact reveal-on-click placeholder. Ships default ON with a
threshold of 5 (adjustable 1–20 in Settings → Content Filters, or off).

Scope
- Pure check (`HashtagSpamCheck`) + settings interface
  (`HashtagSpamSettings`) live in `commons/moderation/`, callable by
  Desktop, `amy` CLI, and (future) Android.
- JVM-backed `PreferencesHashtagSpamSettings` writes to the shared
  `java.util.prefs` node `com/vitorpamplona/amethyst/filters`, so `amy`
  and Desktop observe the same value automatically.
- `CollapsedSpamNote` placeholder in `commons/ui/note/` takes only
  primitive scalars so Android can adopt it without touching commons.
- Desktop wraps every `NoteCard` call site (FeedNoteCard, QuotedNoteEmbed,
  BookmarksScreen, 5 SearchResultsList sites) with a shared
  `SpamCheckedNoteRender` helper. Thread root notes auto-expand via
  `forceReveal=true`; replies still respect the filter.

Exemptions
- Long-form articles (kind 30023)
- Authors in the follow list plus self
- Repost wrappers check the inner event's tags via precomputed
  `note.replyTo`, falling back to `containedPost()`

Search UX fixes bundled in
- Removed the `#hashtag` → "Direct lookup" card. `QueryParser` already
  extracts `#xxx` into the query's hashtag filter, so typing `#bitcoin`
  now goes straight to filtered results.
- Search-result rows now trigger metadata loading via
  `subscriptionsCoordinator.loadMetadataBatched(authors)` and observe
  each user's metadata flow via a new `rememberDisplayData` helper, so
  display names + avatars refresh when kind-0 arrives from index
  relays. Same helper reused in Bookmarks.

Tests + docs
- 19 unit tests (check × 10, displayed-event unwrap × 4, prefs × 5),
  all green.
- Manual testing sheet with 16 scenarios at
  `desktopApp/plans/2026-06-29-hashtag-spam-filter-manual-testing-sheet.md`.
- Plan at `docs/plans/2026-06-29-feat-desktop-hashtag-spam-filter-plan.md`.
- Cross-client desktop feature backlog reference at
  `desktopApp/plans/_desktop-feature-backlog.md`.
2026-07-01 09:46:43 +03:00
Claude 6579b5f656 docs: audit, status-stamp, and index all module plans
Audited all 143 plan files across the 10 plans/ folders. Each plan now
carries a Status header (shipped | in-progress | queued | abandoned)
backed by codebase evidence, and every folder has a README.md index
grouping plans by status.

Shipped plans were moved into a per-folder plans/archive/ (via git mv,
history preserved) so each plans/ folder surfaces only live work:

  shipped (archived): 122   in-progress: 8   queued: 7   abandoned: 4

docs/plans/ is the frozen legacy folder; its plans were stamped and
indexed in place (48 of 52 archived) but it remains closed to new plans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
2026-06-30 15:35:38 +00:00
vitorpamplonaandgithub-actions[bot] a054215c4f chore: sync Crowdin translations and seed translator npub placeholders 2026-06-30 12:51:31 +00:00
vitorpamplonaandgithub-actions[bot] 4060bfac39 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-29 19:43:35 +00:00
vitorpamplonaandgithub-actions[bot] fc95f64458 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-27 16:40:09 +00:00
vitorpamplonaandgithub-actions[bot] cc72e8045a chore: sync Crowdin translations and seed translator npub placeholders 2026-06-27 00:14:28 +00:00
vitorpamplonaandgithub-actions[bot] f5bb04b643 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-26 01:34:37 +00:00
vitorpamplonaandgithub-actions[bot] d841277978 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-25 21:25:00 +00:00
vitorpamplonaandgithub-actions[bot] 49e56da1aa chore: sync Crowdin translations and seed translator npub placeholders 2026-06-25 16:01:36 +00:00
davotoulaandgithub-actions[bot] 8d036e0ac0 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-25 08:12:20 +00:00
vitorpamplonaandgithub-actions[bot] 443bfba65c chore: sync Crowdin translations and seed translator npub placeholders 2026-06-25 00:31:29 +00:00
vitorpamplonaandgithub-actions[bot] 7b890a0eff chore: sync Crowdin translations and seed translator npub placeholders 2026-06-24 14:01:39 +00:00
davotoulaandgithub-actions[bot] dd062861f3 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-23 16:14:26 +00:00
vitorpamplonaandgithub-actions[bot] 832951b90c chore: sync Crowdin translations and seed translator npub placeholders 2026-06-23 15:58:03 +00:00
vitorpamplonaandgithub-actions[bot] 82a20c012a chore: sync Crowdin translations and seed translator npub placeholders 2026-06-23 03:10:29 +00:00
vitorpamplonaandVitor Pamplona bd7c5c78cc chore: sync Crowdin translations and seed translator npub placeholders 2026-06-22 09:38:31 -04:00
vitorpamplonaandgithub-actions[bot] 4aa713e2db chore: sync Crowdin translations and seed translator npub placeholders 2026-06-21 15:36:45 +00:00
vitorpamplonaandgithub-actions[bot] eb04e09d26 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-21 14:32:14 +00:00
vitorpamplonaandgithub-actions[bot] 3dcc46de2b chore: sync Crowdin translations and seed translator npub placeholders 2026-06-21 02:25:11 +00:00