Commit Graph

13991 Commits

Author SHA1 Message Date
Vitor Pamplona 1afa86cb26 Merge pull request #2974 from vitorpamplona/claude/clickable-wallet-card-hHTbM
Add on-chain transaction history screen with pagination
2026-05-19 10:35:31 -04:00
Vitor Pamplona facdd3dc8f Merge pull request #2978 from davotoula/feat/hashtag-limit-plurals-i18n
Convert hashtag-limit message to <plurals>
2026-05-19 10:35:05 -04:00
Vitor Pamplona 439331722a Merge pull request #2973 from vitorpamplona/claude/stop-video-background-timeout-jSLCO
Release MediaController after 30s background timeout
2026-05-19 10:34:44 -04:00
Vitor Pamplona 7b720a9a8d Merge pull request #2977 from davotoula/feat/onchain-zaps-reactions-gallery
Show on-chain Bitcoin zappers as a dedicated ₿ row in the expanded reactions gallery
2026-05-19 10:34:19 -04:00
Vitor Pamplona e94e1782c1 Merge pull request #2975 from nrobi144/fix/desktop-deb-launch-crash
test(desktop): add Compose UI smoke test + release .deb launch CI
2026-05-19 10:33:49 -04:00
Vitor Pamplona a747e20c86 Merge pull request #2980 from vitorpamplona/claude/debug-longpress-root-note-4UR5I
Fix popup menu positioning and parameter naming
2026-05-19 10:01:12 -04:00
Claude f2f02ab5cb fix(thread): restore long-press on root note + anchor popup to the card
Two related fixes to the thread quick-action popup:

- ThreadFeedView.FullBleedNoteCompose declared a `modifier` parameter
  that NoteMaster used to attach `combinedClickable(onLongClick = showPopup)`,
  but the body built a fresh `Modifier.fillMaxWidth().padding(top = 10.dp)`
  for its root Column and discarded the incoming modifier. Long-press on
  the root note in thread view never fired. Spread the incoming modifier
  onto the Column.

- LongPressToQuickAction emitted the Popup as a sibling of the content
  with no wrapping layout. The Popup's `parentLayoutCoordinates` then
  resolved to the enclosing LazyColumn, so `alignment = Alignment.Center`
  centered the menu on the whole list (visually middle of the screen)
  instead of the long-pressed card. Wrap content + Popup in a Box so the
  popup's parent bounds match the note card.
2026-05-19 13:45:56 +00:00
David Kaspar 61a282ba28 Merge branch 'main' into feat/hashtag-limit-plurals-i18n 2026-05-19 15:39:24 +02:00
David Kaspar 5973429860 Merge pull request #2979 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-19 15:38:46 +02:00
Crowdin Bot e1e185a6f2 New Crowdin translations by GitHub Action 2026-05-19 13:26:50 +00:00
Vitor Pamplona de222d152b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-19 09:24:34 -04:00
davotoula 8c3868bc32 i18n: convert hashtag-limit message to <plurals> and add cs/pt-BR/sv/de translations 2026-05-19 14:36:17 +02:00
davotoula cf44c092dd Code review:
- invalidate zaps flow when removeAllChildNotes clears onchainZaps
- simplify on-chain zap gallery after review
2026-05-19 13:10:02 +02:00
davotoula 91ded74636 Show on-chain zappers in expanded reactions gallery
- Add OnchainZappedIcon and PendingClockBadge
- move PendingClockBadge to TopStart to avoid follow-dot clash
2026-05-19 13:10:02 +02:00
David Kaspar d41cf6d75b Merge pull request #2976 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-19 12:34:53 +02:00
nrobi144 5ad21b6acd fix(desktop): disable ProGuard optimization entirely — fixes kmp-tor crash
Disabling individual sub-passes (method/specialization/returntype,
method/marking/static) was not sufficient. The interaction between
multiple optimization passes causes IncompatibleClassChangeError in
kmp-tor's AsyncFs.of() at launch. Shrink (dead code removal) stays
ON for the size win; only optimize is disabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 10:11:11 +03:00
Crowdin Bot 7c3399438d New Crowdin translations by GitHub Action 2026-05-19 06:50:42 +00:00
David Kaspar 733e2945bd Merge pull request #2972 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-19 08:49:16 +02:00
nrobi144 487dd3f2ac fix(desktop): disable method/marking/static ProGuard optimization
The smoke test discovered that ProGuard's method/marking/static pass
converts kmp-tor's AsyncFs.of() from instance to static. The JVM
then throws IncompatibleClassChangeError at launch because callers
still use invokevirtual.

Also updates smoke-test-desktop.yml trigger: runs on any PR touching
desktopApp/ (not just build config files).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 09:48:01 +03:00
nrobi144 c0c055e771 fix(desktop): restore java.management module — confirms #2819 fix
Reverts the intentional breakage from d0a6bbc96. The smoke test
proved it catches the crash: removing java.management from jlink
modules causes a fatal IncompatibleClassChangeError in kmp-tor
at launch (the module is needed for ManagementFactory + tor runtime).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 09:26:26 +03:00
nrobi144 d0a6bbc96f test(ci): intentionally remove java.management to verify smoke test catches #2819
This commit should FAIL the release-deb-launch CI job, proving the
smoke test works. Will be reverted in the next commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 09:05:10 +03:00
nrobi144 2010e41b25 fix(ci): allow dpkg post-install error, verify binary extracted
dpkg -i exits non-zero when the post-install script fails even with
--force-all. Allow the error with || true, then verify the binary
was actually extracted to /opt before proceeding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 07:59:33 +03:00
nrobi144 cca9c2ff3a fix(ci): use dpkg --force-all to skip xdg-desktop-menu error on runner
jpackage's post-install script calls xdg-desktop-menu which fails on
GitHub Actions runners with "No writable system menu directory found".
Menu registration is irrelevant for smoke testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 07:39:34 +03:00
nrobi144 cf6541a1e1 test(desktop): add Compose UI smoke test + release .deb launch CI
Fixes #2819 — v1.08.0 .deb crashed on Ubuntu with ManagementFactory
error because the build had no jlink modules() declaration. Current
main already has the fix; this PR adds CI to prevent regressions.

- Add compose.desktop.uiTestJUnit4 dependency
- DesktopLaunchSmokeTest: renders LoginScreen, asserts title text
- build.yml: xvfb for Linux leg so UI test runs on every PR
- smoke-test-desktop.yml: builds release .deb, installs it, launches
  under xvfb, verifies process stays alive 10s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 07:09:16 +03:00
Claude 7dce3a420f feat(wallet): time-windowed relay sub + coalesce zap bursts
Two improvements to onchain history attribution:

1. Coalesce UI updates. The OnchainZapEvent observation in the
   ViewModel now passes through .sample(500), so a relay flooding
   a backlog of historical zaps in quick succession produces at
   most one map update per 500ms instead of one per event. The
   downstream combine + StateFlow conflation handles the rest.

2. Bound relay queries to the visible window. New
   OnchainZapsFilterAssembler (SingleSubEoseManager-based) wakes
   only while the screen is on top and asks the user's inbox/
   outbox relays for kind-8333 events with since = the oldest
   visible blockTime — incoming (p-tag = user) and outgoing
   (authors = user) — so we don't drag the whole NIP-BC history
   when the user only scrolled through last week. As the user
   pages back, oldestBlockTime drops, the assembler re-subscribes
   with a wider window.

OnchainTransactionsScreen now wires OnchainZapsFilterAssemblerSubscription
with the StateFlow-derived window, and the coordinator owns the
parent assembler so the lifecycle matches other screen subs.
2026-05-19 00:52:12 +00:00
Claude 020d5195b5 feat(wallet): observe onchain zaps reactively, no more per-row scan
OnchainTransactionsViewModel now subscribes to LocalCache via
observeEvents for kind-8333 zaps that either p-tag the user
(incoming) or are signed by the user (outgoing), merging both into
a txid -> OnchainZapEvent map. The FilterIndex inside LocalCache
narrows the fanout so we only wake on relevant events.

The list of chain rows and the zap map are now independent
StateFlows combined into the displayed views, so a zap event
arriving after the page has loaded immediately attributes the
existing row to its Nostr counterparty without a refresh.

Replaces the per-row LocalCache.notes scan from the previous commit.
2026-05-19 00:35:31 +00:00
Claude 4e796c8e8b feat(privacy): plug remaining HTTP paths into the route-aware stack
Three callers were still bypassing PrivacyRouter and would route Tor-only or
direct-only regardless of the picker:

- Coil ImageLoader (AppModules): called
  okHttpClients.getHttpClient(shouldUseTorForImageDownload(it)), which
  collapses to a boolean and can't pick I2P. Now uses
  roleBasedHttpClientBuilder.okHttpClientForImage(it) so images route through
  the user's preferred clearnet transport and hidden-service hostnames
  hard-pin.
- ExoPlayer pool (PlaybackService): had separate poolNoProxy / poolWithProxy
  named fields and bound them to getDynamicCallFactory(useProxy: Boolean) —
  i.e. with-proxy was always the Tor-proxied client. Reshaped to a
  poolsByPort: Map<Int, MediaSessionPool> keyed by the SOCKS port returned
  by proxyPortForVideo(url). DualHttpClientManager gains getHttpClientForPort
  and PortBasedCallFactory to resolve the right OkHttpClient (direct / Tor /
  I2P) live based on port.
- Nostr relay websocket builder (AppModules): only consulted torEvaluator,
  which has no notion of .i2p. Now hard-pins .onion to Tor and .i2p to I2P
  (throwing BlockedRouteException via the fail-closed contract when the
  matching daemon isn't ready) and falls through to torEvaluator for
  clearnet relays — the existing TorRelayEvaluation per-relay-class booleans
  stay as-is for clearnet routing.
2026-05-19 00:16:13 +00:00
Claude 19c750dede feat(privacy): BlockedRouteException — typed fail-closed signal
Replaces the inline IOException thrown by DualHttpClientManager.getHttpClient
when a hidden-service URL is requested while its matching daemon is off.
The new exception extends IOException so existing HTTP error paths (Coil,
OkHttp call factories, etc.) propagate it unchanged, but carries the
BlockReason so future UI work can surface a clear "Enable Tor / I2P to view
this content" hint instead of just a broken-image placeholder.

DualHttpClientManager.blockedException(...) now returns BlockedRouteException
rather than a bare IOException; the messages move into the typed exception's
companion.

Tests cover the message wording (mentions Tor/.onion and I2P/.i2p) and pin
the factory's return type so the typed information can't be silently widened
back to a bare IOException without breaking the test.

Surfacing this to the UI (snackbar on image-load fail, etc.) is left to a
follow-up — that work is cross-cutting (Coil event listeners, NIP-05 error
paths, etc.) and orthogonal to the routing decision itself.
2026-05-19 00:12:55 +00:00
Claude 2a9514d657 refactor(privacy): RoleBasedHttpClientBuilder routes via PrivacyRouter
All ad-hoc HTTP traffic now consults PrivacyRoutingFlow → PrivacyRouter:
images, videos, URL previews, NIP-05 lookups, Money operations, uploads, push
registration. Each call computes a PrivacyRoute (Direct / Tor / I2p /
Blocked) and asks DualHttpClientManager for the matching OkHttpClient,
which in turn picks the right SOCKS-attached client or — for Blocked
hidden-service routes — throws IOException so the request fails closed
instead of silently leaking to the clearnet.

Constructor switched from TorSettingsFlow to PrivacyRoutingFlow. AppModules
swaps the wiring; the flow construction order now puts privacyRouting
before the builder.

Legacy shouldUseTorFor* booleans are kept but now return true iff the route
would actually end up on Tor — they no longer drive routing themselves.
External method-references in AppModules (OtsResolver, etc.) keep working
with the same semantics they had before for clearnet calls.

Push registration still routes through the URL_PREVIEW role (closest analogue
to the old trustedRelaysViaTor global flag, which had no URL to consult).
That's fine for now — trusted-relay push services live on the clearnet and
hard-pin behavior is unchanged for hidden-service push URLs.
2026-05-19 00:11:23 +00:00
Claude e977b96ee6 revert(wallet): drop onchain txid index, scan notes instead
The dedicated ConcurrentHashMap<txid, OnchainZapEvent> in LocalCache
didn't pay for itself at current NIP-BC volumes. The on-chain
transactions view goes back to scanning LocalCache.notes for the
matching OnchainZapEvent per row.
2026-05-19 00:10:53 +00:00
Claude 5da20a9b44 feat(privacy): I2pManager + tri-state DualHttpClientManager
I2pManager: EXTERNAL-only mirror of TorManager. When the persisted I2pType is
EXTERNAL it emits Active(externalSocksPort); OFF and INTERNAL both surface as
Off (no embedded daemon in this branch). Exposes activePortOrNull as a
StateFlow<Int?> with the same shape Tor uses, so the http client managers can
keep the proxy-port wiring symmetric.

DualHttpClientManager + DualHttpClientManagerForRelays:
- New constructor param i2pProxyPortProvider: StateFlow<Int?> (defaults to a
  closed null flow so existing tests / callers compile unchanged).
- Third StateFlow<OkHttpClient> built against the I2P SOCKS port.
- New route-aware methods:
    getHttpClient(route: PrivacyRoute): OkHttpClient
    getCurrentProxyPort(route: PrivacyRoute): Int?
  Direct → no-proxy client, Tor → Tor-proxy client, I2p → I2P-proxy client,
  Blocked → throws IOException with a user-readable reason so the call fails
  closed instead of silently leaking through the clearnet.
- Existing boolean-based methods (getHttpClient/useProxy) untouched.

AppModules wires i2pManager.activePortOrNull into both http managers. No
caller switched onto the new methods yet — that's the next chunk
(RoleBasedHttpClientBuilder reshape).
2026-05-19 00:09:29 +00:00
Vitor Pamplona ab3d1dd7e5 fix(quartz/sqlite): set busy_timeout to deflake reader+writer races
Without busy_timeout SQLite returns SQLITE_BUSY immediately when
BEGIN IMMEDIATE can't acquire a lock — e.g. during a WAL
auto-checkpoint or a reader briefly upgrading its snapshot — instead
of retrying. ParallelInsertTest's reader+writer test hit this ~10%
of runs. 5s matches Room's default and adds no overhead in the
uncontended case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:02:35 -04:00
Claude a42a156f13 feat(privacy): extend PrivacyOptionsScreen with I2P section and clearnet picker
Same Privacy Options screen as before, now also surfaces:
- A three-way "Privacy Network" picker (Direct / Tor / I2P) for clearnet
  traffic. Hidden services aren't affected — .onion / .i2p still hard-pin to
  their matching daemon (and fail closed via PrivacyRouter when it's off).
- An I2P engine + per-feature toggle section mirroring the existing Tor
  section. Only OFF and EXTERNAL are offered in the picker until an embedded
  daemon ships; persisted INTERNAL is treated as OFF in the UI but kept in the
  enum so a future commit can light it up without a migration.
- Save action posts all three settings — Tor, I2P, preferred transport — to
  their separate DataStore-backed writers in one click.

New UI pieces:
- I2pDialogViewModel — mirror of TorDialogViewModel, no preset DSL since
  there's no canonical I2P configuration yet
- I2pSettingsBody — composable mirror of PrivacySettingsBody, shares
  SwitchSettingsRow with the Tor body
- PrivacyTransportPickerViewModel + PreferredTransportPicker — global picker
- I2pType.resourceId — Android string-id extension matching TorType.resourceId

Strings: i2p_* per-feature toggles, i2p_off/internal/external, i2p_socks_port,
privacy_clearnet_transport (+ direct / tor / i2p labels).

UI is wired against the live flows from AppModules (torPrefs / i2pPrefs /
privacyPrefs from the previous commits). No HTTP routing change — those flows
still go through TorManager only; the new toggles only have routing impact
once DualHttpClientManager grows tri-state in the daemon chunk.
2026-05-18 23:35:37 +00:00
Crowdin Bot cfa19ce7da New Crowdin translations by GitHub Action 2026-05-18 23:22:26 +00:00
Vitor Pamplona 0d91245e02 Merge pull request #2971 from vitorpamplona/claude/add-nowhere-links-LPMnM
Add Nowhere link detection and branded card rendering
2026-05-18 19:20:34 -04:00
Claude 4d428025d3 feat(voice): pause voice notes on background like videos
Voice notes go through GetVideoController, so the 30s background
release timer + warm-pool reattach already applied to them. What was
missing was the immediate ON_PAUSE handler: voice didn't use
ControlWhenPlayerIsActive, so playback kept running for the full 30s
before the release timer hit and cut it off via the warm-pool pause.

Extract a small PauseControllerWhenInBackground composable from the
ON_PAUSE arm of ControlWhenPlayerIsActive and call it from VoiceTrack.
Result matches video: pause on ON_PAUSE, release at 30s, reassemble
the controller (paused, position preserved) when the user returns —
no auto-resume since voice has no autoplay setting.

PiP exemption (BackgroundMedia.isMutex) is preserved.

https://claude.ai/code/session_01RoEUbAN8ejF21Ns3eM6xad
2026-05-18 23:18:51 +00:00
Claude b16d44721c perf(richtext): collapse nowhere-link classifier into the existing URL branches 2026-05-18 22:54:35 +00:00
Claude acf3daaf75 feat(wallet): tappable onchain rows + txid index in LocalCache
- LocalCache now keeps a ConcurrentHashMap<txid, OnchainZapEvent>
  populated in consume(OnchainZapEvent). First sender to claim a
  txid wins. The on-chain transactions ViewModel switches to
  LocalCache.getOnchainZapByTxid(txid), replacing the per-row scan
  of notes.
- Each row in OnchainTransactionsScreen is now clickable: rows with
  a matched zap event navigate to the zap's note thread; the rest
  open the transaction on mempool.space in the system browser.
2026-05-18 22:54:04 +00:00
Claude b71b7cb420 fix(video): skip background release timer for PiP
The 30s release timer fires on ON_PAUSE, but PipVideoActivity enters
PiP mode (which dispatches ON_PAUSE) immediately after onCreate. The
MediaController is built asynchronously and RegisterBackgroundMedia
runs only after inner() mounts, so there's a window where the timer's
BackgroundMedia.isMutex check sees a stale (or null) bgInstance and
releases a controller that was about to start background playback —
blanking the PiP window.

Add an opt-out parameter on GetVideoController and disable the timer
from PipVideoActivity. PiP IS the explicit background-playback opt-in,
so the timer doesn't apply by design.

https://claude.ai/code/session_01RoEUbAN8ejF21Ns3eM6xad
2026-05-18 22:50:33 +00:00
Vitor Pamplona 8b31e60f52 Merge pull request #2965 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-18 18:36:46 -04:00
Claude a2da9ae2c6 i18n: extract NowhereLinkCard tool labels to string resources 2026-05-18 22:33:23 +00:00
Claude fdd954c2ed feat(privacy): PrivacyRoutingFlow facade reading from live pref flows
Read-side seam that snapshots TorSettingsFlow + I2pSettingsFlow + the
preferredClearnetTransport StateFlow into a PrivacySettings, then delegates
to PrivacyRouter.route. Gives the I2P daemon manager and the eventual HTTP
routing rewrite a single stable API to consult instead of poking every flow
themselves.

DualHttpClientManager is not touched in this commit — that grows to tri-state
in the chunk where the I2P daemon lands a real proxy port.

AppModules:
- Constructs `privacyRouting` alongside roleBasedHttpClientBuilder
- Uses torPrefs.value + i2pPrefs.value + privacyPrefs (added last commit)

No callers wired yet — this is the seam those callers will move onto.
2026-05-18 22:31:26 +00:00
Crowdin Bot 37f6f92c04 New Crowdin translations by GitHub Action 2026-05-18 22:14:57 +00:00
Vitor Pamplona 68c081602f Merge pull request #2966 from vitorpamplona/claude/fix-video-mime-type-sqkiz
Fix MediaStore MIME type compatibility for video files
2026-05-18 18:13:19 -04:00
Claude 09857b8924 Merge remote-tracking branch 'origin/main' into claude/fix-video-mime-type-sqkiz 2026-05-18 22:04:15 +00:00
Vitor Pamplona af4835aff3 Merge pull request #2967 from vitorpamplona/claude/notification-settings-refactor-FZ1bx
Extract notification settings to dedicated screen
2026-05-18 17:58:04 -04:00
Claude b2b4895570 refactor(notifications): cleaner Compose patterns in Categories section
- Replace `remember { side effect }` with lazy `entry.ensure(context)` in
  the row's onClick; the system per-channel page only needs the channel
  to exist at open time, not before, and the ensure call is idempotent.
- Replace the `refreshKey++` invalidation trick with a real
  `Map<channelId, ChannelStatus>` state holder updated inside
  `LifecycleResumeEffect`; downstream reads are direct map lookups.
- Replace `Triple`-with-destructuring in `ChannelStatusBadge` with three
  direct `when` branches calling `StatusChip` — no tuples, no
  temporaries.
- Drop the unused `notification_settings_open_system` string.
2026-05-18 21:53:32 +00:00
Claude b4ef70176e test: cover MIME type normalization for MediaStore
Verify video/x-m4v maps to video/mp4 (the failing case), normalization
is case-insensitive, and supported image/video/audio types pass through
unchanged. Required making normalizeMimeTypeForMediaStore internal.
2026-05-18 21:51:58 +00:00
Claude 4d88444a59 feat(notifications): split delivery vs display, add Categories section
Reorganize Notification settings into three sections that reflect what
each control actually does:

- Delivery: how notifications reach the device — push provider (fdroid)
  and the always-on relay service live together here.
- In-app display: how the notifications screen renders incoming activity —
  currently the Split-by-Follows toggle.
- Categories: one row per user-facing Android NotificationChannel (DMs,
  Mentions, Replies, Reactions, Zaps, Chess, Scheduled posts, Calls),
  showing the current importance (On / Silent / Off) and opening the
  system per-channel settings page on tap. Foreground-service channels
  are intentionally omitted — disabling them breaks the service contract.

The screen ensures every listed channel exists on first open and
re-reads importance via LifecycleResumeEffect so the badge reflects
changes made in system settings.

API surface bumped for the channel registry:
- CallNotifier.CALL_CHANNEL_ID is now public.
- ScheduledPostNotifier.ensureChannel is now public.
2026-05-18 21:49:09 +00:00
Vitor Pamplona d3c9f5a4f0 Merge pull request #2970 from vitorpamplona/claude/fix-timedout-exception-TJ2te
Refactor media upload to use AccountViewModel.launchSigner
2026-05-18 17:47:47 -04:00