Commit Graph
14222 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub 522a83c4de Merge pull request #3078 from vitorpamplona/claude/pensive-turing-6UC1W
Add dedicated NIP-82 software app detail screen
2026-05-27 18:38:03 -04:00
Claude a2b4e5fa70 feat(nip82): compact apps feed card + dedicated detail screen
Rework the NIP-82 Software Applications feed item so it scans cleanly at
list density and split the detail content into its own route.

Feed card (RenderSoftwareApplication): icon + name + summary, full
description (3 lines), platforms / license chips, and a latest-version
chip resolved from LocalCache. Drops the screenshots strip, website /
repo link rows, and #topic chips at feed scale. The card is tappable
and the standard ReactionsRow now hosts replies, boosts, likes, zaps,
and share underneath each card.

New SoftwareAppDetailScreen (Route.SoftwareAppDetail) reached via the
card tap, the routeFor() dispatch, and naddr deep links. Layout: 72dp
header, screenshots carousel, About, Platforms, Topics (#tag chips
clickable through to Route.Hashtag), Links, ReactionsRow, latest
release with bundled assets, a collapsible "show older releases"
section, and the NIP-22 comment thread inline (driven off the app's
address tag through ThreadFeedViewModel + ThreadFilterAssembler).
2026-05-27 22:23:59 +00:00
Vitor PamplonaandGitHub 097f9a9a5b Merge pull request #3077 from mstrofnone/feat/desktop-namecoin-core-rpc-backend
feat(desktop): Namecoin Core RPC backend + composite fallback
2026-05-27 16:07:01 -04:00
m d9d7b44e91 feat(desktop): Namecoin Core RPC backend + composite fallback
Brings Amethyst Desktop to feature parity with Android for the Namecoin
resolution backend stack landed in #3056 / #3068. Three pieces:

- DesktopNamecoinPreferences now persists backend, namecoinCoreRpc,
  fallbackToCustomElectrumx and fallbackToDefaultElectrumx (KEY_BACKEND
  / KEY_CORE_RPC / KEY_FALLBACK_*), mirroring NamecoinSharedPreferences.
  Mutators are non-suspend because java.util.prefs is synchronous,
  unlike Android's coroutine-backed DataStore. The Jackson mapper now
  rejects unknown properties on read so kotlinx `@Serializable`
  computed getters (e.g. NamecoinCoreRpcConfig.isUsable) round-trip
  cleanly through java.util.prefs.

- DesktopNamecoinNameService takes an optional OkHttpClient provider,
  lazily constructs a NamecoinCoreRpcClient when supplied, and builds
  a fresh CompositeNamecoinBackend per lookup based on current
  NamecoinSettings. Same shape as AppModules#buildNamecoinBackend.
  Exposes the underlying RPC client (rpcClient) and a probeCoreRpc(cfg)
  helper for the Settings Test RPC button.

- NamecoinSettingsSection gains a backend radio selector, a Core RPC
  subform (URL / username / password / Save / Test RPC) with a TOFU
  cert-pin AlertDialog mirroring Android's NamecoinCoreRpcSection, and
  a fallback toggles section. The same KEY_PINNED_CERTS list is shared
  with both ElectrumXClient and NamecoinCoreRpcClient via
  setDynamicCerts(...), matching Android's behaviour where both
  backends consume one trust store.

- Main.kt wires DesktopHttpClient.currentClient() in as the Core RPC
  HTTP provider so .onion RPC URLs flow through the existing Tor
  routing without extra plumbing, and propagates the new mutators to
  the Settings UI.

- Extends DesktopNamecoinPreferencesTest with 8 new cases covering
  default state, backend round-trip, Core RPC URL/user/pass/pin-flag
  round-trip, fallback toggles, reset clearing, and a full
  multi-field round-trip across a fresh preferences instance.

Verification on the canonical workspace clone:
- ./gradlew :commons:jvmTest --tests *Namecoin* — BUILD SUCCESSFUL
- ./gradlew :amethyst:compilePlayDebugKotlin — BUILD SUCCESSFUL
- ./gradlew :desktopApp:compileKotlin :desktopApp:test — BUILD SUCCESSFUL
- ./gradlew :amethyst:testPlayDebugUnitTest --tests *Namecoin* — BUILD SUCCESSFUL
- ./gradlew :amethyst:spotlessCheck :commons:spotlessCheck
  :desktopApp:spotlessCheck — BUILD SUCCESSFUL
2026-05-28 06:01:54 +10:00
Vitor PamplonaandGitHub f898e4be57 Merge pull request #3067 from vitorpamplona/claude/confident-allen-AOGU6
Add music tracks and playlists support with NIP-51 events
2026-05-27 15:59:41 -04:00
Claude bf02a235e0 Merge remote-tracking branch 'origin/main' into claude/confident-allen-AOGU6
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
2026-05-27 19:55:07 +00:00
Vitor PamplonaandGitHub 08520f553f Merge pull request #3076 from mstrofnone/feat/desktop-namecoin-pinned-certs
feat(desktop): persist TOFU-pinned Namecoin ElectrumX certs
2026-05-27 15:45:19 -04:00
m b5b70fe693 feat(desktop): persist TOFU-pinned Namecoin ElectrumX certs
Mirrors Android's NamecoinSharedPreferences pinned-cert API on Desktop so
user-accepted TLS pins survive process restart. Same JSON-list shape, same
distinct-append semantics, same wipe-on-reset behaviour.

What's new
- DesktopNamecoinPreferences gains addPinnedCert / loadPinnedCerts /
  clearPinnedCerts (sync rather than suspend, since java.util.prefs is
  synchronous). reset() now clears pinned certs too, matching Android.
- DesktopNamecoinNameService accepts a pinnedCertsProvider and pushes the
  loaded list into ElectrumXClient.setDynamicCerts at init, mirroring
  Android's AppModules.kt wiring. Exposes the underlying client so the
  Settings UI can call testServer() and re-apply pins live.
- Desktop NamecoinSettingsSection grows an optional Test Connection + TOFU
  pin sub-section: runs ElectrumXClient.testServer per active server,
  collects PEM + SHA-256 fingerprint from successful TLS handshakes, and
  prompts the user to pin each new cert via AlertDialog. UI hidden when
  no service is wired (so existing call sites stay valid).
- Main.kt wires both halves together and updates the freshly-pinned cert
  list into the live client without waiting for restart.

Persistence is plain java.util.prefs (same backing store as the rest of
DesktopNamecoinPreferences) — explicitly NOT EncryptedSharedPreferences.
Pinned cert PEMs are public material; no secrets stored.

Tests
- DesktopNamecoinPreferencesTest: +6 cases covering empty default,
  persistence + reload, dedup, blank input ignored, reset wipes, and
  independence from settings copies.

Verification
- ./gradlew :desktopApp:compileKotlin — BUILD SUCCESSFUL
- ./gradlew :desktopApp:test — BUILD SUCCESSFUL (16 tests, 0 failures)
- ./gradlew :amethyst:compilePlayDebugKotlin — BUILD SUCCESSFUL
- ./gradlew :amethyst:spotlessCheck :commons:spotlessCheck :desktopApp:spotlessCheck — BUILD SUCCESSFUL

Stack note
Stacked behind #3072 (merged 2026-05-27). Next: PR-C for the full
Namecoin Core RPC backend + composite fallback persistence + UI.
2026-05-28 05:41:22 +10:00
David KasparandGitHub 7c4220ba3d Merge pull request #3073 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-27 21:41:18 +02:00
Claude 8a55bba1f2 feat(music): parallel uploads + survive-screen-leave + visible progress
Two issues with the send flow on the new-music-track screen:

 1. Cover + audio uploaded sequentially. As cover finished, its picker
    reverted to the placeholder while audio was still going, making the
    user think the operation was done.
 2. The coroutine ran on the screen's viewModelScope, so leaving the
    screen cancelled it.

Rework the upload pipeline:

 * Snapshot the form into an immutable SendSnapshot before launching, so
   user edits to the form during the in-flight upload don't poison what
   gets published, and the coroutine sees stable inputs even after the
   per-screen VM is cleared.
 * Run cover + audio uploads in parallel via async/awaitAll. Either
   side failing throws an UploadException that the outer catch routes
   to the global toast manager.
 * Launch through accountViewModel.launchSigner so the work runs on the
   AccountViewModel.viewModelScope — survives screen leave. Errors and
   success both surface as global toasts.
 * Keep coverMedia/audioMedia/pickedAudioName populated through the
   ENTIRE upload+publish, not just per-phase. Only clear them on
   success, so each picker keeps showing the user the file it's still
   uploading.
 * Single isSending flag replaces the separate isUploading/isPublishing
   booleans; drives an inline progress banner + disables the Send
   button + dims the pickers to read-only while in flight.
 * One-shot completionEvents SharedFlow that the screen subscribes to.
   If the user is still on the screen when the operation finishes, we
   popBack; if they've already left, no one is listening and we don't
   pop someone else's stack frame.

Also tighten the playback Row height in MusicTrack from 100dp to 80dp.
With a 75dp play button the previous 100dp container left a ~12.5dp
empty band top + bottom that the user flagged as too tall.
2026-05-27 19:39:50 +00:00
Vitor PamplonaandGitHub 80991c6c8b Merge pull request #3074 from vitorpamplona/claude/gallant-darwin-VfniN
Fix route hint re-firing on activity recreation
2026-05-27 15:33:28 -04:00
Vitor PamplonaandGitHub e937b460e1 Merge pull request #3072 from mstrofnone/feat/commons-namecoin-settings-extension
refactor(namecoin): consolidate NamecoinSettings into commons
2026-05-27 15:31:19 -04:00
Claude 187cb407c9 fix(new-user): stop Import Follow List from reappearing after dismiss
The route hint set by newKey() (Route.ImportFollowsSelectUser) was stored
on AccountState.LoggedIn and never cleared. LoggedInPage re-assigned
accountViewModel.firstRoute = route on every recomposition, so any
Activity recreation (rotation, dark-mode toggle, returning from
background) made the screen pop up again even after the user dismissed
it via popBack.

Consume the route once via remember(state) in AccountScreen, and gate the
firstRoute assignment behind LaunchedEffect(Unit) so it only fires once
per composition lifetime.
2026-05-27 19:26:28 +00:00
Claude 3082095a4b feat(music): auto-fill metadata from picked audio + drop audio URL field
Five changes:
 1. Drop the audio-URL text field from the composer. Upload tile at the
    top is the single source of truth; the ViewModel still threads the
    existing url through MusicTrackEvent.edit so edit mode keeps it.
 2. Auto-fill title / artist / album / duration from MediaMetadataRetriever
    when an audio file is picked. Only fills empty fields — anything the
    user already typed wins. Duration is the exception: derived numbers
    overwrite, since they're not opinions.
 3. Rename the SavingTopBar 'Save' button to 'Send' for this screen
    (introduces SendingTopBar + R.string.send so other screens can reuse).
 4. Fix silent data corruption in AddToMusicPlaylistViewModel.init:
    the previous `if account already set, return` guard kept the FIRST
    track address the VM ever saw. Subsequent invocations with a
    different trackAddress (process recreation, navigation reusing the
    back-stack entry) routed toggle() at the stale track, adding the
    wrong track to playlists. Now refreshes trackAddress on every call
    and restarts the scan job when it changes.
 5. Inset modifier order on NewMusicTrackScreen tightened in the prior
    commit, kept here for continuity.
2026-05-27 19:18:41 +00:00
Claude 9c9cf8844d fix(music): save audio to MediaStore.Audio (not Video) and trim composer
Three fixes in one:

1. MediaSaverToDisk crash when saving a music URL. The fall-through
   branch routed every non-image, non-PDF MIME to
   MediaStore.Video.EXTERNAL_CONTENT_URI; an audio/mpeg blob inserted
   into the Video collection fails with IllegalArgumentException. Add a
   dedicated audio/* branch that writes to MediaStore.Audio with
   DIRECTORY_MUSIC.

2. Drop the redundant cover-URL text field from the new-music-track
   composer. The upload tile at the top is the source of truth for the
   cover — having a second field at the bottom asked the user to set
   the same thing twice. ViewModel still threads coverUrl through to
   MusicTrackEvent.edit so editing an existing track keeps its image.

3. Tighten the new-music-track Scaffold's inset modifier order:
   padding(pad) -> consumeWindowInsets(pad) -> imePadding() ->
   horizontal padding -> verticalScroll. Without consumeWindowInsets,
   imePadding double-counts the nav-bar inset on Android 11+ and the
   bottom field gets shoved too far up when the keyboard opens.
2026-05-27 19:08:54 +00:00
Crowdin Bot 2c9d9c3129 New Crowdin translations by GitHub Action 2026-05-27 19:08:46 +00:00
Vitor PamplonaandGitHub 58f1ee9606 Merge pull request #3069 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-27 15:07:14 -04:00
Vitor PamplonaandGitHub 63b91012fb Merge pull request #3071 from vitorpamplona/claude/trusting-mccarthy-SA1aI
Add Software Apps feed with follow list filtering
2026-05-27 15:07:05 -04:00
m 67edb32fa7 refactor(namecoin): consolidate NamecoinSettings into commons
Two NamecoinSettings classes had drifted:

- commons (used by Desktop): only enabled + customServers
- amethyst service.namecoin (used by Android): full schema with backend,
  namecoinCoreRpc, fallbackToCustomElectrumx, fallbackToDefaultElectrumx

This left Desktop unable to persist any of the Namecoin Core RPC or
fallback-policy state introduced in the Android settings UI. Promote
the rich Android version into commons as the single source of truth
and delete the Android duplicate.

- Move the rich schema (backend, namecoinCoreRpc, fallback toggles,
  hasUsableCoreRpc, toFallbackPolicy) into the commons NamecoinSettings.
- Delete amethyst/service/namecoin/NamecoinSettings.kt and its test.
- Repoint the two Android imports (NamecoinSharedPreferences,
  NamecoinSettingsSection) at the commons class. No behaviour change on
  Android.
- Fold the Android-only backend/RPC/fallback test cases into the commons
  NamecoinSettingsTest so the shared schema stays covered.

Desktop persistence (DesktopNamecoinPreferences) still only reads/writes
enabled + customServers; the extra commons fields fall back to defaults
on the existing Desktop store. Wiring those new fields into Desktop is
the next change.
2026-05-28 05:06:44 +10:00
Vitor PamplonaandGitHub 7692bee0a5 Merge pull request #3070 from mstrofnone/fix/namecoin-search-id-d-prefixes
fix(search): route d/ and id/ Namecoin namespaces through the resolution row
2026-05-27 15:06:10 -04:00
m 216176eeb7 fix(search): route d/ and id/ Namecoin namespaces through the resolution row
The inline Namecoin resolution row in the global search bar (and the
on-chain zap recipient field) is gated by looksLikeNamecoinIdentifier,
which previously only matched the '.bit' shapes. Direct namespace
references like 'id/mstrofnone' or 'd/mstrofnone' — both accepted by
NamecoinNameResolver.isNamecoinIdentifier — fell through the gate and
the resolver was never called, so no on-chain feedback appeared in the
search bar.

Bring the UI gate in line with the resolver:

  - accept 'd/<name>' (domain namespace, direct reference)
  - accept 'id/<name>' (identity namespace)
  - lower the length floor so single-character labels (valid, if
    expensive, on chain) still trigger; '.bit' inputs keep their
    5-char floor

Doc comment for the row's behaviour and the NamecoinResolutionRow
KDoc are updated to reflect the broader accepted set. New unit tests
cover both new prefixes (with case-insensitivity and leading '@'),
short single-label names, bare-prefix rejection, and explicit
non-routing for other Namecoin namespaces ('a/', 'u/').
2026-05-28 04:46:55 +10:00
Claude ea227f1968 fix(music): playlist cover chip is wrap-content again, not full-cover
The previous cover used SubcomposeAsyncImage with a single content lambda
that stacked the loaded image and the chip as BoxScope siblings. Under
Coil's subcompose-layout pass the chip's wrap-content Box got measured
to the cover's full size, painting a translucent black square over the
whole artwork.

Drop SubcomposeAsyncImage. Use rememberAsyncImagePainter + a plain
Image + painter-state observation inside a normal Box. The image fills
the box (Modifier.matchParentSize) and the chip sits as a normal
BoxScope child with Modifier.align(BottomStart).padding(12.dp) —
predictable wrap-content sizing.
2026-05-27 18:33:22 +00:00
Claude 506ed94bfd fix(software-apps): add top-nav filter and route NIP-82 events through LocalCache
The Apps screen was missing the top-nav follow-list spinner that other
single-feed screens (Articles, Longs, Pictures) use, and nothing was
loading because the relay subscription only queried the user's own
outbox and `SoftwareApplicationEvent` (kind 32267) was never consumed
by `LocalCache.justConsumeInnerInner` — it fell through to the
"Event Not Supported" else branch and got dropped. `ReleaseArtifactSetEvent`
(kind 30063) and `SoftwareAssetEvent` (kind 3063) had the same gap.

- Register `SoftwareApplicationEvent`, `SoftwareAssetEvent`, and
  `ReleaseArtifactSetEvent` in the `LocalCache` consume dispatch.
- Add `defaultSoftwareAppsFollowList` setting (Account/Settings/Prefs)
  and `liveSoftwareAppsFollowLists{,PerRelay}` flows.
- Reshape `SoftwareAppsSubAssembler` to extend
  `PerUserAndFollowListEoseManager<_, TopFilter>` and assemble per-list
  relay filters via `makeSoftwareAppsFilter`, mirroring Pictures/Longs.
- Add a `SoftwareAppsTopBar` with the standard `FeedFilterSpinner` and
  a `WatchAccountForSoftwareAppsScreen` to invalidate the DAL when the
  selected list changes.
- DAL now applies `FilterByListParams` so the rendered feed honors the
  active top-nav list.
2026-05-27 18:25:39 +00:00
Crowdin Bot 2b6798e78a New Crowdin translations by GitHub Action 2026-05-27 18:25:22 +00:00
Vitor PamplonaandGitHub d5cca1e3e4 Merge pull request #3068 from mstrofnone/feat/namecoin-core-rpc-tofu
feat(namecoin): TOFU pin for Namecoin Core RPC TLS path
2026-05-27 14:23:25 -04:00
mstrofnone a1071b5189 fix(namecoin): move setConfig into the bootstrap launch
Addresses review feedback on PR #3068: keep the entire NamecoinCoreRpcClient
bootstrap (config push + pinned-cert load) inside the single applicationIOScope
launch instead of doing setConfig synchronously in the by-lazy block. Matches
the ElectrumXClient init shape above.
2026-05-28 04:18:15 +10:00
Claude 4177390591 feat(music): swap bookmark rows for playlist row in track dropdown menu
Music tracks (kind 36787) belong in playlists (kind 34139), not in the
generic bookmark list — but the dropdown menu was showing both, which
made the bookmark rows feel like the default place to save tracks.

Mirror the EmojiPack pattern instead: one curation flow per kind. For a
MusicTrackEvent the Timestamp & Bookmarks section now shows only
'Add to playlist' (which opens the toggle sheet that already handles
add/remove across every owned playlist). Everything else still gets the
standard Manage / Private bookmark / Public bookmark trio.
2026-05-27 18:18:11 +00:00
Claude 82d2290e43 feat(music): cover image and audio file pickers on new-track composer
The new-track composer was URL-only — users had to upload audio and
artwork elsewhere and paste links. Match the New Badge composer flow
instead: pick the files up front, Save uploads them through the user's
default Blossom/NIP-96 server and publishes the kind 36787 event with
the resulting URLs.

Cover image picker is the first item on the screen (square upload tile,
same style as Badge). Audio file picker sits below it. URL text fields
stay below the pickers so power users who source audio from Wavlake /
Stemstr / their own server can still paste links instead of uploading.

ViewModel changes:
 - coverMedia / audioMedia: MultiOrchestrator slots fed by the pickers
 - saveAndPublish() runs cover upload then audio upload then publish;
   either failure short-circuits and surfaces a toast
 - isValid() accepts either a picked audio file or a non-blank URL
 - All Compose state writes from IO hop through Main.immediate

Audio picker uses OpenDocument(audio/*) — we don't reuse the shared
FileSelect because it also accepts application/pdf.
2026-05-27 18:11:44 +00:00
Claude 71d50504fd feat(music): split tracks and playlists into separate filter pipelines
Up until now both screens shared one combined REQ that asked for both
kinds (36787 + 34139). That meant a single 'makeMusicTracksFilter' had
to cover both feeds, and the since cursor had to be the min of both
feeds' lastNoteCreatedAt to avoid over-fetching.

Split the pipeline so each screen owns its own kind:
 - filterMusicEventsByX helpers are parameterized by kinds: List<Int>
 - MUSIC_TRACK_KINDS = [36787], MUSIC_PLAYLIST_KINDS = [34139]
 - makeMusicTracksFilter(...) and makeMusicPlaylistsFilter(...) are
   thin wrappers picking the right kind list
 - MusicTracksSubAssembler / MusicPlaylistsSubAssembler each use their
   own helper and their own feed's cursor (no min-of-both)

Also:
 - Cover the rest of the top-bar selectors that were previously falling
   through to emptyList: Hashtag, Geohash (Location), AllCommunities,
   SingleCommunity. Music feeds now respect all of them.
 - Drop TimeUtils.oneWeekAgo() floor from filterMusicEventsGlobal —
   music kinds are sparse on most relays, the floor silently hid older
   content the user hadn't seen yet.

Tracks referenced by a playlist are still loaded on demand by each
PlaylistTrackRow's own observeNoteEvent, so the playlists REQ no longer
needs to bundle kind 36787.
2026-05-27 17:52:28 +00:00
m cfb3f1b9fa feat(namecoin): TOFU pin for Namecoin Core RPC TLS path
When the user picks the Namecoin Core RPC backend and points at a
self-hosted node behind a self-signed cert (StartOS / Start9, umbrel,
LAN reverse proxy, …) the previous flow only worked if the cert's CA
was already in the device trust store. There was no in-app way to
inspect or pin the certificate, so users had to install the StartOS
root CA at the OS level — or settle for an unencrypted onion path.

This change brings the Namecoin Core RPC path up to parity with the
existing ElectrumX path:

  - NamecoinCoreRpcClient.probe() now opens a short-lived, no-auth TLS
    socket alongside the JSON-RPC call to capture the server's leaf
    certificate (PEM + SHA-256 fingerprint). Capture is best-effort
    and only runs for https:// URLs. Credentials are never sent over
    the inspection socket.

  - RpcProbeResult exposes serverCertPem, certFingerprint, and
    tlsHandshakeFailed so the Settings UI can react. New fields are
    nullable / default false so existing callers compile unchanged.

  - NamecoinCoreRpcClient maintains its own dynamic-cert keystore and
    a lazy pinned SSLSocketFactory (same shape as ElectrumXClient's,
    minus the hardcoded list — Core RPC has no public defaults). When
    cfg.usePinnedTrustStore is true and the URL is https, callRpc()
    routes through the pinned factory with a permissive hostname
    verifier (LAN/onion certs commonly carry IP-only SANs).

  - NamecoinSettingsSection's Namecoin Core RPC card now shows a
    'Trust Server Certificate?' AlertDialog after Test RPC when the
    probe captured a cert and the user hasn't pinned yet, reusing the
    existing namecoin_pin_cert_* strings. Accept persists the PEM
    AND flips usePinnedTrustStore=true on the config. The result
    card also displays the captured fingerprint and a '(pinned)'
    marker so the user can see the current trust state at a glance.

  - The pinned PEM list is stored in the existing
    KEY_PINNED_CERTS DataStore entry, so a single TOFU confirmation
    covers both backends. AppModules' namecoinCoreRpcClient init now
    bootstraps the pinned list on app start, matching ElectrumX.

  - Tests cover the new probe fields' defaults and the addPinnedCert
    / setDynamicCerts surface.

Local verification: builds clean (assembleFdroidDebug), :quartz:jvmTest
NamecoinCoreRpcClientTest all green, :amethyst:testFdroidDebugUnitTest
namecoin suites all green.
2026-05-28 03:32:28 +10:00
Claude e876a162a4 fix(music): review feedback round
- isPublic() now always returns !isPrivate() so a playlist tagged with
  both public=true and private=true is consistently reported as private,
  matching the 'isPrivate wins' contract documented on isPrivate().
- AddToMusicPlaylistViewModel + NewMusicPlaylistFab: hop to
  Dispatchers.Main.immediate for every Compose State write. The wrapping
  coroutines (rescan loop, launchSigner) run on Dispatchers.IO; Snapshot
  tolerates off-main writes but the codebase convention is main-only.
- MusicTracksSubAssembler + MusicPlaylistsSubAssembler: the single REQ
  asks both kinds 36787+34139, so the since cursor must be the min of
  both feeds' lastNoteCreatedAt to avoid over-fetching the lagging kind.
  Both assemblers now also listen to the other feed's cursor flow.
- Extract formatTrackDuration into MusicFormatting.kt; MusicTrack and
  MusicPlaylist share it.
- syntheticWaveformFor: replace inline FQN
  com.vitorpamplona.amethyst.service.playback.composable.WaveformData
  with an import.
- NewMusicPlaylistFab: drop the second .trim() — the dialog's confirm
  button already trims before invoking onCreate.
2026-05-27 17:27:44 +00:00
Claude 5a4cb5cc16 fix(music): apply avatar+chip pattern to loading and error covers too
The previous fix only ported the no-cover branch. Loading and error
fallbacks were still rendering banner-only with the chip floating alone,
hiding the author's avatar entirely whenever a cover URL was set but
hadn't loaded.

Drop the MyAsyncImage wrapper here and drive SubcomposeAsyncImage
directly so every painter state picks its own overlay:
 - Success: real cover + floating chip alone (no avatar to collide with)
 - Loading: blurred banner + avatar+chip side-by-side
 - Error / no image: banner + avatar+chip side-by-side
2026-05-27 17:21:00 +00:00
Claude 93a27abf23 fix(music): playlist cover chip no longer collides with author avatar
When a playlist has no `image` tag, the cover falls back to the
author's profile banner with their avatar baked into the bottom-left by
DefaultImageHeader. The track-count chip was also pinned to BottomStart,
so it landed on top of the avatar.

Render the banner alone (DefaultImageBanner) and place the avatar and
the chip side-by-side in a single bottom row instead. Extract the chip
into a small TrackCountChip composable so both branches share the same
visual.
2026-05-27 16:28:10 +00:00
Claude a403f44d8b fix(music): observe playlist and track notes so cards update on relay arrival
The playlist header and each PlaylistTrackRow were reading note.event
synchronously, so when a fresh playlist (or a track referenced by the
playlist) arrived from a relay after first composition, the row stayed
on its 'Unknown track' / stale-snapshot placeholder until the parent
recomposed for some other reason.

Switch both to observeNoteEvent<T>, which subscribes to the note's
metadata flow AND drives the EventFinderFilterAssembler — so a relay
delivering the event both updates the local cache and triggers
recomposition on the same call.
2026-05-27 15:36:30 +00:00
Vitor PamplonaandGitHub 1cd57bd497 Merge pull request #3066 from vitorpamplona/claude/sweet-maxwell-UMahG
fix(playback): hide video controls while the error overlay is shown
2026-05-27 11:27:18 -04:00
Claude b2c76b7428 fix(playback): hide video controls while the error overlay is shown
Tapping the player while the codec-not-supported overlay is up would still
toggle the gradients/buttons in. Gate the controls block on a clear error
state so the overlay stays the only thing on screen.
2026-05-27 15:13:25 +00:00
Claude 3f3ccd88c8 Merge remote-tracking branch 'origin/main' into claude/confident-allen-AOGU6 2026-05-27 15:12:43 +00:00
Claude 0b083b75aa fix(music): playlists feed uses its own follow-list subscription
The playlists screen was reusing the tracks subscription, which is keyed
on the tracks follow list. If the user picked a different list in the
playlists spinner, the REQ ignored it and the feed showed empty whenever
the two lists disagreed.

Add a dedicated MusicPlaylistsFilterAssembler/SubAssembler keyed on
defaultMusicPlaylistsFollowList so the spinner change actually rewires
the REQ. The filter still asks for both kinds 36787 + 34139 per relay so
opening this screen alone is enough to populate cached tracks.
2026-05-27 15:10:24 +00:00
Vitor PamplonaandGitHub bbc7f9740e Merge pull request #3065 from vitorpamplona/claude/gifted-ritchie-5XjZf
Exclude author from zap split display logic
2026-05-27 11:09:29 -04:00
Vitor PamplonaandGitHub 7d6bbac200 Merge pull request #3064 from vitorpamplona/claude/sweet-maxwell-UMahG
Add playback error overlay with browser fallback for video codec failures
2026-05-27 11:04:09 -04:00
Claude e876e2b09b feat(zap-splits): hide single-author zap split row in NoteCompose
When a note's only zap split recipient is the post author, the split is
redundant — the author already receives the zap. Skip rendering the row
in those cases by gating on a new `hasZapSplitSetupBesidesAuthor` helper.
2026-05-27 14:54:48 +00:00
Claude 89607376cc feat(playback): surface unsupported codec errors with browser fallback
ExoPlayer entered the ERROR state silently when a codec was missing or the
container/format wasn't supported, leaving a blank video area with no
recourse. Track the player error in MediaControllerState, render an overlay
with the error code, and offer an "Open in browser" button so the user can
fall back to the system browser for codecs the device can't decode.
2026-05-27 14:51:09 +00:00
Claude 0139eb207c fix(music): subscribe each playlist track to relays so they actually load
LoadAddressableNote only resolves the AddressableNote shell from cache —
it doesn't kick off a relay query. So a freshly-arrived playlist whose
track events weren't already in the cache sat on the "Loading…"
placeholder row forever.

For each resolved track shell, wire up EventFinderFilterAssemblerSubscription
(same pattern ProfileBadgesScreen uses for badge definitions). Its
NoteEventLoaderSubAssembler asks relays for the addressable's actual
event, and EventWatcherSubAssembler picks up reactions / replies on top.
The subscription is keyed on the AddressableNote and lifecycle-aware, so
scrolling the playlist off-screen drops the watchers.
2026-05-27 14:49:02 +00:00
Claude 30b06773e8 fix(music): dedupe playlist description vs content when identical
Some publishing tools copy the same blurb into both the `description` tag
and the JSON `content` field of a kind-34139 playlist. The renderer was
showing both, producing a duplicate paragraph stacked on itself.

When the trimmed strings match (case-insensitively), suppress the short
description tag and keep the content field — `content` goes through
TranslatableRichTextViewer below, which handles Markdown and NIP-19
references, so it's the richer of the two to keep.
2026-05-27 14:42:38 +00:00
Claude 3e4f204d1f feat(music): standalone Playlists feed + simpler add-to-playlist picker
Two changes that go together.

1) Dedicated Playlists feed screen.

   A new MusicPlaylistsScreen sits alongside MusicTracksScreen with the
   same mechanics — FeedContentState, top-bar follow-list spinner,
   FAB, refreshable + lifecycle-aware feed loader — but renders kind-34139
   playlists instead of kind-36787 tracks. Each row goes through NoteCompose
   so it picks up the existing RenderMusicPlaylist path (cover, track-count
   chip, descriptions, track rows).

   - MusicPlaylistsFeedFilter pulls kind 34139 from LocalCache.addressables,
     keyed on a new account-settings flag defaultMusicPlaylistsFollowList
     so the spinner choice is independent from the tracks feed.
   - Account exposes liveMusicPlaylistsFollowLists + …PerRelay.
   - LocalPreferences persists the new setting (key
     "defaultMusicPlaylistsFollowList") so it survives a relaunch.
   - The screen mounts MusicTracksFilterAssemblerSubscription — the same
     REQ already fetches both kinds, so opening the playlists tab on its
     own is enough to populate the cache.
   - NewMusicPlaylistFab opens an AlertDialog asking for just a name and
     publishes an empty MusicPlaylistEvent. Users then add tracks via the
     existing "Add to playlist" sheet on any track note.
   - Route.MusicPlaylists, NavBarItem.MUSIC_PLAYLISTS (in drawer Feeds
     section), ScrollStateKeys.MUSIC_PLAYLISTS_SCREEN, and the new strings
     route_music_playlists / new_music_playlist round out the wiring.

2) Add-to-playlist sheet stops imitating bookmarks.

   The previous sheet copied the bookmark-management pattern — TopAppBar,
   FAB → AlertDialog, ListItem rows with leading icon stack and a trailing
   round add/remove IconButton. That UI exists because bookmarks juggle a
   public/private split; playlists don't, so those affordances were just
   visual noise that didn't fit.

   The new sheet is a quick picker:
     - "Create new playlist" row inlined at the top (name field + Create).
     - LazyColumn of compact rows: a check-circle icon when the track is
       already in that playlist, the playlist-add icon when it isn't,
       playlist title, and the track count below in a small caption.
     - Tap anywhere on the row to toggle membership.
     - Bottom link "Manage all playlists" → Route.MusicPlaylists, so users
       who want richer management hop over to the new feed screen.

   Drops MusicPlaylistManagementItem.kt (bookmark-style ListItem) and the
   five strings that only existed for that file's leading-icon labels.
2026-05-27 14:35:40 +00:00
Vitor PamplonaandGitHub d381cf9109 Merge pull request #3063 from davotoula/feat/avif-support
Comprehensive AVIF support (#837)
2026-05-27 10:32:03 -04:00
Claude d837132247 fix(music): drop boilerplate content, fuse cover+player, clickable hashtags
Four UI polish fixes that all touch RenderMusicTrack:

1) Suppress the "Listen to my song …" auto-fill text.

   Several Blossom uploaders pre-populate the kind-36787 content field
   with that exact prefix — it just restates the title/artist that the
   card already shows above. MUSIC_TRACK_BOILERPLATE_PREFIXES is the
   one place to extend if more publishing tools start producing similar
   noise.

2) Cover and player now read as a single piece of UI.

   Previously the cover wore top-rounded corners and the audio player
   (via RenderAudioWithWaveform) used `imageModifier`'s all-rounded
   chrome plus a 5dp top padding — so two visually disconnected blocks
   sat above and below a gap.

   The audio player is now inlined: GetMediaItem → GetVideoController →
   RenderVoicePlayer with a custom border modifier that rounds only the
   bottom corners. No top padding. The result reads as one continuous
   card with the cover up top and the player below.

3) Internal DisplayUncitedHashtags row is gone.

   RenderAudioWithWaveform renders its own hashtag row for voice
   messages (where it's the only chip display). MusicTrack already
   renders TopicChips externally, so the inner row was producing a
   second, visually-different row of the same chips. Inlining the
   player chain (point 2) also drops that row.

4) TopicChip is clickable.

   Tapping `#electronic` now navigates to Route.Hashtag("electronic") —
   the same destination RichTextViewer's inline hashtags go to. The chip
   adds an optional `onClick` parameter so existing callers (and the
   preview block) keep working unchanged.
2026-05-27 14:19:08 +00:00
Claude e510fcceca fix(music): synthetic waveform now actually varies per track
The previous version drew envelope and carrier as pure functions of the
sample position — so 60% of every bar was identical across all tracks and
only the per-bar jitter (40% weight, narrow range) wiggled. Every track
ended up with the same slow-fade-sine silhouette and only the noise
pattern differed, which looked like every waveform was the same shape.

Now every shape parameter — phase offset, carrier frequency, baseline,
envelope strength, noise envelope — is drawn from the seeded RNG before
the per-bar loop. Two different track ids produce visibly different
waveforms: some have many tight bars, some few wide ones, some fade in
and out, others stay flat. The seed is also bit-shuffled with a Knuth
multiplicative constant XOR'd with the id length so two ids whose Java
hashCode happens to collide still diverge.

ExoPlayer still owns playback progress; the bars are purely decorative.
2026-05-27 14:12:01 +00:00
davotoula ef25f8c0e6 test(amethyst): instrumented coverage for AVIF upload + decode
Adds 4 instrumented test files + 3 tiny pre-committed AVIF fixtures to
catch regressions in the upload pipeline.
2026-05-27 16:11:20 +02:00
davotoula f8b24c645a fix(chat): hide DM quality slider for AVIF and correct error framing 2026-05-27 16:11:20 +02:00