Commit Graph
14190 Commits
Author SHA1 Message Date
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
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
davotoula 7d580452e4 fix(uploads): surface specific AVIF metadata error instead of 'Upload cancelled' 2026-05-27 16:11:20 +02:00
davotoula 50a81c35cf Code review:
style(nests): import TimeUtils in CreateNestViewModel instead of inline FQN

HIGH-1: import java.io.RandomAccessFile in MetadataStripper instead of
inline fully-qualified name

HIGH-2: catch AvifMetadataNotVerifiableException in the 6 ViewModels
that call MetadataStripper.strip directly (profile picture, emoji pack
list+display, bookmark group, nest, channel)

MEDIUM-1: tighten AvifAnimatedDecoderFactory.createAnimatedImageDecoder
annotation from @RequiresApi(P) to @RequiresApi(S); the outer guard is
already SDK_INT < S.

MEDIUM-2: replace the curried lambda DI seam in MetadataStripper with
a named fun interface (AvifExifReader).

MEDIUM-3: rename isGifUrl -> isAnimatedMediaUrl (MyAsyncImage) and
BaseMediaContent.isGif() -> isAnimatedMedia() (ZoomableContentView)
since both predicates now cover AVIF as well as GIF.

- AvifAnimatedDecoderFactory.isAvif now iterates a single brand list
  with .any { rangeEquals(8, it) } instead of three || branches.
- MetadataStripper.inspectAvifMetadata dropped the outer defensive
  try/catch; the inner catch already converts parse failures to
  AvifMetadataNotVerifiableException and the rest of the function
  cannot realistically throw.
- PreviewMetadataCalculator extracts the shared ImageDecoder allocator
  + exception path from decodeAvifBytes and decodeAvifFromUri into a
  single private decodeAvif(source) helper.
- RobohashFallbackAsyncImage merges its identical Loading and Error
  when branches into one via Kotlin's multi-value branch syntax.
- MediaCompressorTest drops a no-op MockKAnnotations.init(this) call
  and the now-unused import; no @MockK fields exist.
2026-05-27 16:11:20 +02:00
davotoula 03c42f585e AVIF display + thumbnail-cache fixes from manual testing
fix(ui): default avatar contentScale to Crop, not Fit
fix(images): skip thumbnail cache for animated AVIF profile pictures
fix(ui): animate profile pictures regardless of URL extension
2026-05-27 16:11:20 +02:00
davotoula 57724cee8c Comprehensive AVIF support (#837)
feat(ui): hide compression slider for non-compressible files (AVIF, GIF, SVG)
feat(images): custom Coil decoder for animated AVIF
feat(ui): include AVIF in animation-aware MIME predicates
fix(uploads): AVIF extension fallback in BlossomUploader
fix(uploads): AVIF extension fallback for NIP-96 multipart filename
feat(uploads): decode AVIF previews with ImageDecoder for blurhash/thumbhash
feat(uploads): fail-closed AVIF metadata inspection in MetadataStripper
fix(uploads): preserve AVIF bytes through MediaCompressor
feat(uploads): add MediaMimeTypes helper for AVIF detection
2026-05-27 16:11:20 +02:00
davotoulaandClaude Opus 4.7 adc0d36407 docs(amethyst): TDD-style implementation plan for AVIF support (issue #837)
15 bite-sized tasks across 6 phases (A foundation, B upload pipeline, C animation
lifecycle audit, D test fixtures + instrumented tests, E manual on-device
verification, F ship). Each task has exact file paths, full test code, full
patch code, exact commands, expected output, and per-task commits.
Companion to amethyst/plans/2026-05-26-avif-support.md spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:11:20 +02:00
davotoula a99927bf92 Docs: plan for comprehensive AVIF support (issue #837)
docs(amethyst): document strip-toggle-off AVIF EXIF leak as known limitation
docs(amethyst): document Desktop AVIF gaps from spot-check
docs(amethyst): record animated AVIF playback caveats from on-device testing
docs(amethyst): note API < 31 gallery-picker greys out AVIF (OS limit)
docs(amethyst): tighten API < 31 known-limitation with on-device findings
docs(amethyst): plan and design for AVIF instrumented tests
2026-05-27 16:11:20 +02:00
Claude 5f2f70b0bf fix(music): subscribe to relays, fake waveform, dedupe hashtag row
Three follow-ups against testing the Scatman track event.

1) Music feed pulls from relays now.

   The "doesn't load anything even on Global" bug: there was no relay
   subscription wired for kind 36787 / 34139, so the feed only showed
   tracks that happened to already be in LocalCache (own publishes,
   direct nostr: links, hashtag visits). Mirrors the LongsFilterAssembler
   stack:

     - MusicTracksFilterAssembler + MusicTracksSubAssembler register a
       per-user-and-follow-list subscription on the music screen and
       whenever MUSIC_TRACKS is pinned to the bottom bar.
     - SubAssemblyHelper.makeMusicTracksFilter dispatches the active
       TopFilter to the right per-relay filter set:
         · Global  → unscoped kind 36787/34139 from outbox/proxy relays
         · AllFollows / Authors / MutedAuthors → scoped to the active
           author set
       Hashtag/Geohash/Community variants fall through to emptyList for
       now; add per-case handlers when the spinner grows those routes.
     - RelaySubscriptionsCoordinator owns the assembler instance.
     - LocalPreferences persists defaultMusicTracksFollowList (key
       "defaultMusicTracksFollowList") so the spinner's choice survives a
       relaunch like every other feed.

2) Synthetic waveform replaces the empty audio strip.

   Kind 36787 has no `waveform` tag in the spec, so RenderAudioWithWaveform
   was given `null` and showed a flat audio bar. Now seeded off the track
   address: each track keeps the same decorative shape across recompositions,
   and a sine envelope + carrier + jitter makes the result read as "music
   waveform" rather than pure noise. ExoPlayer still owns playback progress
   — this is purely visual.

3) Hashtags now render in exactly one row.

   The Scatman test case (4 `t` tags including "music") was showing both
   DisplayUncitedHashtags (every `t`) and a TopicChip FlowRow (every `t`
   except the "music" genre marker), so the user saw visually-different
   duplicate chip rows. Dropped DisplayUncitedHashtags from MusicTrackHeader;
   TopicChips is the canonical music-aesthetic chip row and already
   excludes the genre marker.
2026-05-27 13:56:33 +00:00
Vitor PamplonaandGitHub 1ef8b7405a Merge pull request #3060 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-27 09:56:19 -04:00
Crowdin Bot 72afadbb38 New Crowdin translations by GitHub Action 2026-05-27 13:20:50 +00:00
Vitor PamplonaandGitHub b12e160457 Merge pull request #3056 from mstrofnone/feat/namecoin-core-rpc-backend
feat(namecoin): add Namecoin Core RPC backend with optional ElectrumX fallback
2026-05-27 09:17:30 -04:00
Vitor PamplonaandGitHub 7af34762bb Merge pull request #3062 from vitorpamplona/claude/payment-targets-ui-7pgrY
feat(profile): toast when no app handles a payment target scheme
2026-05-27 09:15:29 -04:00
Claude 023a3c6624 feat(profile): toast when no app handles a payment target scheme
Tapping a chip silently failed if no installed app handled the
type-specific URI scheme (bitcoin:, ethereum:, monero:, etc.).
Surface that case through the existing toastManager so users know
to install a compatible wallet.

https://claude.ai/code/session_01R7kRziq14Hc22dPwAnZRAr
2026-05-27 13:10:36 +00:00
Claude 9724931355 feat(music): voice-style audio player for audio-only tracks + follow-list spinner
Two related polish passes after comparing the music UI against the rest of
the codebase.

1) Voice-message audio player wins for audio-only tracks

   The Voice (NIP-A0) renderer's RenderAudioWithWaveform is the right
   playback widget for a kind-36787 track that has no `video` URL — it's
   ExoPlayer-backed (same as VideoView) but exposes:
     - inline tap-to-show controls with play/pause centered
     - top buttons for Share / Save-to-gallery / Picture-in-Picture / Mute
     - a 100dp compact strip (vs. VideoView's 16:9 video surface that
       leaves a blank rectangle for audio-only streams)
     - waveform-aware (we pass null since kind 36787 has no `waveform` tag)

   The new layout for an audio-only track:
     - album-art cover at the top (square, 1:1)
     - compact audio player below
     - title / artist / meta below that
   When a `video` URL is set the track keeps the full VideoView path —
   that's a real music-video file and deserves the video surface.

2) Music feed top bar now has a follow-list spinner

   Every sibling feed (Articles, Longs, Polls, Pictures, etc) exposes a
   FeedFilterSpinner backed by its own settings flag. The music screen
   previously only showed a static "Music" title and silently reused the
   Home follow list, so users couldn't filter the music feed independent
   of Home.

   - AccountSettings: defaultMusicTracksFollowList (defaults to Global) +
     changeDefaultMusicTracksFollowList(name) setter pair.
   - Account: liveMusicTracksFollowLists + …PerRelay flows, sourced from
     the new setting via the existing topNavFilterFlow machinery.
   - MusicTracksFeedFilter switches its filter params + feedKey to the
     dedicated flow.
   - MusicTracksTopBar swaps the static Text for FeedFilterSpinner with
     the same kind3GlobalPeopleRoutes catalog Articles/Longs use (All
     Follows, Your Follows, kind3 Follows, Around Me, Global, custom
     people lists, interest sets, mute list).
   - WatchAccountForMusicTracksScreen now watches liveMusicTracksFollowLists
     so list switches invalidate the feed.
2026-05-27 13:10:13 +00:00
Vitor PamplonaandGitHub 9856458c54 Merge pull request #3061 from vitorpamplona/claude/payment-targets-ui-7pgrY
feat(profile): modern chip layout for payment targets
2026-05-27 09:09:09 -04:00
Claude 62943d8f0a fix(music): preserve tags on edit/toggle, lock concurrent toggles, search, mute
Audit pass turned up several real bugs in the music feature. Rolled the
fixes into one commit since they all touch the publish/edit path.

Data-loss bugs (must-fix)

  - NewMusicTrackViewModel.publish() in edit mode rebuilt the event via
    MusicTrackEvent.build() with only the composer-visible fields, silently
    dropping every other tag the original event carried — video URL,
    released, track_number, format, bitrate, sample_rate, language,
    explicit, extra `t` genre tags, zap splits, anything custom. Adds a
    MusicTrackEvent.edit(earlierVersion, ...) companion that clones the
    existing TagArray and only mutates composer-managed fields, mirroring
    PinListEvent/BookmarkListEvent's `add`/`remove`/`resign` pattern.
  - AddToMusicPlaylistViewModel.toggle() and .createWithTrack() had the
    same problem for playlists. Adds MusicPlaylistEvent.addTrack /
    removeTrack companions that preserve the rest of the tag array. The
    VM now uses these instead of round-tripping through build().

Correctness bugs (must-fix)

  - MusicPlaylistEvent.build() emitted only one of `public`/`private`
    while isPublic() defaulted to `true`-when-absent. A private playlist
    therefore round-tripped as isPublic() && isPrivate(). isPublic() now
    falls back to !isPrivate(), and build() still emits the explicit pair
    so unrelated clients reading either flag agree on visibility.
  - AddToMusicPlaylistViewModel's `isWorking` flag wasn't a concurrency
    primitive — fast taps on different rows could race and the loser's
    broadcast would replace the winner's. Adds a Mutex around toggle /
    createWithTrack so they serialize.
  - The initial rescan() ran on the composition thread (init() is called
    from the sheet's body). Both initial and live re-scans now run on
    Dispatchers.IO inside the same Job, and the live collector filters
    bundles by kind so we don't re-walk LocalCache for every Text Note.
  - NewMusicTrackViewModel.init() set `isEditing = true` based purely on
    `editDTag != null`, so a stale dTag pointing at no cached event left
    the user on a Delete button that no-op'd. `isEditing` now derives
    from loadedEvent and falls back to create-mode when the lookup misses.
  - MusicTrackHeader synthesized mimeType "video/${format ?: "mp4"}" when
    a `video` URL was present — but `format` is the AUDIO format per
    spec, so a track with both `video=...mp4` and `format=mp3` emitted
    "video/mp3". Pass null when videoUrl wins and let ExoPlayer sniff.
  - MusicTracksFeedFilter only consulted params.match(), which checks
    the follow list but not mute/spammer/word lists. Muted authors leaked
    through. Mirror LongsFeedFilter and AND `account.isAcceptable(note)`.

Search & idiomatic fixes (should-fix)

  - MusicTrackEvent + MusicPlaylistEvent now implement SearchableEvent so
    the local SQLite FTS indexes title/artist/album/description rather
    than only the JSON content. Searching "Pink Floyd" by artist now
    matches the local cache instead of waiting for relay results.
  - MusicTracksFeedFilter.feedKey() now class-prefixes with "music-" so it
    can't collide with other feeds that key off the same Home follow list.
  - MusicPlaylistEvent.build() renamed `description`/`shortDescription` to
    `content`/`description` so the parameter names match the spec.

Nits

  - Drop trackCount() — callers prefer `trackAddresses().size`.
  - Drop six unused strings (composer placeholders for not-yet-wired
    Blossom audio/cover upload UI).
  - Wrap preview runBlocking { justConsume(...) } in remember{} so it
    runs once per preview key instead of every recompose.
  - Use a hex-shaped id for preview events instead of "track_xxx_yyy".
2026-05-27 11:10:09 +00:00
Vitor PamplonaandGitHub 7e44df0d1f Merge pull request #3059 from davotoula/feat/emoji-pack-add-to-list-menu
Add "Add/remove to/from emoji list" row to pack-card menu
2026-05-27 06:34:10 -04:00
Vitor PamplonaandGitHub da5f01011c Merge pull request #3058 from nrobi144/feat/desktop-profile-editing
feat(desktop): full profile editing — 13 fields, image upload, NIP-05 verification, drag-and-drop
2026-05-27 06:33:26 -04:00
davotoula 205b629c9d Code review:
- tighten EmojiListToggleRow null-handling and label branching
2026-05-27 09:58:26 +02:00
davotoula 142bf67678 Add "Add to emoji list" row to pack-card menu
Closes the UX gap where a user who creates a pack via the in-app UI has no
path to add it to their NIP-51 kind-10030 selection without leaving the
pack-management screens.
2026-05-27 09:58:03 +02:00
nrobi144andClaude Opus 4.6 bcf61d53ff feat(desktop): add drag-and-drop for avatar/banner, fix avatar overlay
- Wire DragAndDropTarget on avatar circle and banner area
- Image-only filter (jpg/png/gif/webp/avif)
- Visual drag-over feedback (primary border highlight)
- Fix avatar: only show placeholder icon when no image set
  (previously overlay was visible behind the loaded avatar)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 10:22:18 +03:00
m daa7c7913e feat(namecoin): mention umbrel alongside StartOS in docs and UI hints
Both umbrelOS (via getumbrel/umbrel-apps#4962) and StartOS / Start9
(via Start9-Community/namecoin-core-startos) ship a self-hosted
Namecoin Core that this backend can target. Generalize the
help/strings so umbrel users discover the feature too.

No logic changes.
2026-05-27 14:50:37 +10:00
m a58e7164b5 feat(namecoin): add Namecoin Core RPC backend with optional ElectrumX fallback
Adds a second resolution backend alongside the existing ElectrumX path:
users can now point Amethyst directly at a Namecoin Core full node
(e.g. a StartOS / Start9 installation) instead of (or in addition to)
trusting public ElectrumX operators.

Settings -> Namecoin grows three new pieces:

  1. Backend selector (radio) - ElectrumX | Namecoin Core RPC
  2. Core RPC section - URL, username, password, masked password,
     'Test RPC' button that calls getblockchaininfo and reports
     chain / height / sync %, error path with diagnostic message
  3. Fallback policy - independent toggles for falling back to the
     user's custom ElectrumX servers (Core RPC primary only) and/or
     the hardcoded public ElectrumX defaults

Quartz additions:
  - NamecoinBackend enum, NamecoinCoreRpcConfig (kotlinx.serialization),
    NamecoinFallbackPolicy
  - NamecoinNameBackend interface + ElectrumxNameBackend adapter +
    CompositeNamecoinBackend orchestrator (implements IElectrumXClient
    so NamecoinNameResolver is unchanged)
  - NamecoinCoreRpcClient (jvmAndroid) - JSON-RPC name_show /
    getblockchaininfo over OkHttp, reuses
    roleBasedHttpClientBuilder.okHttpClientForNip05() so Tor onion
    endpoints work without extra plumbing

Semantics:
  - Authoritative negatives (NameNotFound, NameExpired) short-circuit
    the chain - no silent privacy leak to other backends
  - Only transport / unreachable failures cascade through the chain
  - All fallback toggles default off (custom servers stay exclusive,
    matching existing behaviour)
  - Settings persisted via NamecoinSharedPreferences DataStore
  - HTTP transport delegated to roleBasedHttpClientBuilder so existing
    Tor/proxy/cert pinning all works for Core RPC too

Tests:
  - CompositeNamecoinBackendTest (8 cases) - short-circuit, cascade,
    authoritative-negative, electrumx-primary path, expired-name,
    random-exception-cascade
  - NamecoinCoreRpcClientTest (7 cases) - success parsing, auth header,
    name-not-found, expired, generic RPC errors, unusable config,
    probe success + auth failure
  - NamecoinSettingsTest (8 cases) - parser plus new backend / fallback
    fields

Builds clean: :amethyst:compileFdroidDebugKotlin, :quartz:jvmTest.
2026-05-27 14:03:47 +10:00
nrobi144andClaude Opus 4.6 ce173debba fix(desktop): center avatar vertically and increase to 120dp
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 06:51:35 +03:00
nrobi144andClaude Opus 4.6 2e405f5644 fix(desktop): redesign avatar picker as tappable circle with upload overlay
Replace the awkward small icon button with a full 100dp tappable circle.
Shows surfaceVariant background when empty, semi-transparent overlay with
centered upload icon when image is present. Spinner replaces icon during
upload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 06:46:29 +03:00
nrobi144 b608ca02fb Merge remote-tracking branch 'upstream/main' into feat/desktop-profile-editing
# Conflicts:
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt
2026-05-27 06:38:47 +03:00
Claude 1e3d2bbd3d feat(profile): modern chip layout for payment targets
Replace plain text payment-target rows with a FlowRow of pill-shaped
clickable chips that carry a type-aware icon, brand color, uppercase
label, and a truncated address. Tap opens a type-specific URI scheme
(bitcoin:, lightning:, ethereum:, monero:, liquidnetwork:, dash:,
payto:// fallback) so wallets can actually pick up the intent; long-press
still copies the authority to the clipboard.
2026-05-27 02:55:15 +00:00
Claude 8424140d60 chore(music): @Preview composables for every new UI surface
Adds previews wherever there's something visual worth previewing:

- MusicTrack.kt: RenderMusicTrack (full event with cover + meta),
  RenderMusicTrackExplicit (explicit-badge variant), MusicTrackCover (no
  cover fallback), ExplicitBadge, TopicChip. Uses the same
  Event-constructor + LocalCache.justConsume + LoadNote pattern Attestation
  and Wiki previews already use.
- MusicPlaylist.kt: RenderMusicPlaylist (with three resolved track refs),
  RenderMusicPlaylistCollaborativePrivate (chips visible),
  MusicPlaylistCover, MissingPlaylistTrackRow, TrackCoverPlaceholder,
  PlaylistTag.
- MusicTracksScreen.kt: full feed scaffold via mockAccountViewModel().
- MusicTracksTopBar.kt: the static-title top bar.
- NewMusicTrackButton.kt: the FAB on its own.
- NewMusicTrackScreen.kt: the composer form.
- AddToMusicPlaylistSheet.kt: NewMusicPlaylistDialog leaf, and the empty
  state of the full sheet.
- MusicPlaylistManagementItem.kt: four variants — not-in / in /
  empty (zero tracks) / untitled.

Skipped MusicTracksFeedLoaded — FeedState.Loaded can't be easily mocked
and no existing feed-loaded previews exist in the codebase. The screen
preview already covers the scaffold around it.

URLs in the constructed events point at example.invalid so MyAsyncImage
falls back to the deterministic DefaultImageHeader robohash and VideoView
shows its tap-to-load thumbnail state — both of which are the real
first-paint state users see before tapping anything.
2026-05-27 02:51:36 +00:00
Claude 2210341291 refactor(music): align Add-to-Playlist with bookmark-management UI
Compared the music-playlist-management sheet against the existing
PostBookmarkListManagementScreen and noticed the playlist sheet was the odd
one out — custom TopAppBar with a Done action, inline TextField + Button row
for creating new lists, custom Row + Checkbox per playlist, no way to tap
into a playlist to view it.

The bookmark screen uses a richer Material3 pattern that's already familiar
to users. Aligning the music sheet to it:

- TopBarWithBackButton (back arrow + title) replaces the bespoke top bar.
- Scaffold FAB → NewListButton opens a small AlertDialog with a name field
  (lighter than the bookmark route to a full edit screen, but consistent in
  affordance). The previous inline create-row is gone.
- Each row is now a MusicPlaylistManagementItem mirroring
  BookmarkGroupManagementItem: leading icon + total-track-count chip,
  headline title, supporting "In this playlist" / "Not in this playlist"
  status text, trailing round IconButton (red Remove / blue Add).
- Tapping the row navigates into the playlist's note view (Route.Note with
  the addressable's tag); tapping the trailing button toggles membership.
  Previously these were collapsed into a single whole-row tap that toggled
  but never let the user actually see the playlist.

ViewModel unchanged — the same toggle()/createWithTrack() operations now
just feed a more conventional UI.
2026-05-27 02:37:52 +00:00
Claude 058f42b6e8 feat(music): full-screen feed, composer with FAB, add-to-playlist sheet
Builds three discrete user-facing surfaces on top of the kind 36787 / 34139
quartz events:

1. Music feed screen (Route.MusicTracks)
   - MusicTracksFeedFilter pulls every kind-36787 addressable that passes the
     user's Home follow list + hidden/blocked rules. Reuses liveHomeFollowLists
     rather than introducing a new AccountSettings flag (deferred to a future
     iteration that wants its own spinner).
   - MusicTracksScreen mirrors LongsScreen's scaffold: bottom-nav-aware top
     bar, refreshable feed list, FAB. Each row renders through NoteCompose so
     reactions/zaps/replies behave like Home.
   - Discoverable via the side drawer (DrawerFeedsItems) and pinnable to the
     bottom bar (NavBarItem.MUSIC_TRACKS).

2. New-music-track composer (Route.NewMusicTrack)
   - NewMusicTrackViewModel mirrors NewCalendarCollectionViewModel: title /
     artist / audio URL / cover URL / album / duration / lyrics fields,
     dTag-preserving edit mode, NIP-09 deletion. Publishes via
     account.signAndComputeBroadcast(MusicTrackEvent.build(...)).
   - NewMusicTrackButton FAB wires the music screen entry to the composer.
   - MVP intentionally skips audio-file Blossom upload from inside the
     composer — users paste URLs they uploaded elsewhere. NewMediaModel
     wiring can be added later without disturbing the rest of the flow.

3. Add-to-playlist sheet (Route.AddToMusicPlaylist)
   - AddToMusicPlaylistViewModel scans LocalCache.addressables for the user's
     own kind-34139 playlists (filterIntoSet(kind, pubKey)) and offers
     toggle-membership + create-with-track operations. Each mutation
     re-signs the playlist with the existing dTag so the address points at
     the new ordered track list.
   - AddToMusicPlaylistSheet renders a checkbox list with an inline
     "new playlist" creator row.
   - DropDownMenu exposes the entry from the …-menu on any MusicTrackEvent
     note via the existing M3ActionRow row pattern.

String resources added under values/strings.xml. Drawer + nav-bar catalog
entries cover the new route. BottomBarFeedPreloaders adds a documented `Unit`
arm for MUSIC_TRACKS so the exhaustive `when` stays exhaustive without
forcing a relay-subscription file before there's logic to put in it.
2026-05-27 02:27:20 +00:00
Vitor PamplonaandGitHub 4306f26460 Merge pull request #3055 from vitorpamplona/claude/affectionate-wright-GRP8r
fix(user-metadata): fall back to indexer relays when outbox is exhausted
2026-05-26 21:55:32 -04:00
Claude db9f4da394 fix(music): cover IS the player — tap actually starts playback
The previous layout stacked a non-functional static cover (with a decorative
play-button overlay) above the real VideoView, so tapping the prominent UI
element did nothing while the actual playback widget sat smaller below.

Use LoadThumbAndThenVideoView / VideoView as the primary header so the
album art is the player's own thumbnail and ExoPlayer handles the
tap-to-play / streaming. Falls back to a plain cover only when the event
has no playable URL at all (data-integrity case).
2026-05-27 01:28:15 +00:00
Claude db2b9551f9 fix(user-metadata): fall back to indexer relays when outbox is exhausted
If a user's NIP-65 outbox advertises only relays that don't hold their
kind 0, profile fetching used to give up after EOSE on those relays.
filterUserMetadataForKey now widens to the account's indexer relays
once every outbox relay has either EOSE'd or is in cannotConnectRelays
and metadata is still missing. UserWatcherSubAssembler invalidates
filters on EOSE so the fallback re-evaluates without waiting for an
unrelated trigger.
2026-05-27 01:20:37 +00:00