From 3b9cca19cf5341a47c5b7691093c5968111806e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 21:14:55 +0000 Subject: [PATCH] feat: add "Mine" option to music + playlist feeds; fix cover in editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes: 1. "Mine" top-nav filter for the Music and Playlists feeds. Adds a shared `musicRoutes` option list (the content-style catalog plus "Mine") to TopNavFilterState and points both music top bars at it. The local-cache feed filters and the relay sub-assemblers now handle TopFilter.Mine by restricting to the logged-in user's own tracks/playlists (by author, over their outbox relays) — same pattern as the badges/communities feeds. 2. Fix: editing a track/playlist showed the empty upload placeholder even when the event already had a cover. The shared CoverImagePicker now renders the already-published cover URL (with tap-to-replace and a remove button) when no new local file is picked. The track composer's clearPickedCover now also clears the saved URL so "remove cover" sticks on save. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013oofoSH7eMXrs2TCU4uncS --- .../amethyst/ui/screen/TopNavFilterState.kt | 23 ++++ .../loggedIn/music/MusicComposerUploadUi.kt | 110 +++++++++++++++--- .../loggedIn/music/MusicPlaylistsTopBar.kt | 5 +- .../loggedIn/music/MusicTracksTopBar.kt | 8 +- .../loggedIn/music/NewMusicPlaylistScreen.kt | 1 + .../loggedIn/music/NewMusicTrackScreen.kt | 1 + .../loggedIn/music/NewMusicTrackViewModel.kt | 3 + .../music/dal/MusicPlaylistsFeedFilter.kt | 25 +++- .../music/dal/MusicTracksFeedFilter.kt | 25 +++- .../datasource/MusicPlaylistsSubAssembler.kt | 8 ++ .../datasource/MusicTracksSubAssembler.kt | 8 ++ .../subassemblies/FilterMusicEventsMine.kt | 54 +++++++++ 12 files changed, 238 insertions(+), 33 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/subassemblies/FilterMusicEventsMine.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 71c615ca6c..88e99ce5c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -292,6 +292,24 @@ class TopNavFilterState( ) } + private val _musicRoutes = + combineTransform( + livePeopleListsFlow, + liveInterestFlows, + ) { peopleLists, interests -> + checkNotInMainThread() + emit( + listOf( + // Same content-style catalog as kind3GlobalPeopleRoutes, plus "Mine" so the + // music + playlists screens can show only the user's own published items. + listOf(allFollows, userFollows, kind3Follows, aroundMe, globalFollow, mineFollow), + peopleLists, + interests, + listOf(muteListFollow), + ).flatten().toImmutableList(), + ) + } + private val _kind3GlobalPeople = livePeopleListsFlow.transform { peopleLists -> checkNotInMainThread() @@ -341,6 +359,11 @@ class TopNavFilterState( .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, persistentListOf(allFollows, userFollows, kind3Follows, globalFollow, mineFollow, muteListFollow)) + val musicRoutes = + _musicRoutes + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, globalFollow, mineFollow, muteListFollow)) + fun destroy() { Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt index 492e294fb5..0048e10406 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -31,6 +32,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme @@ -39,9 +41,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import coil3.compose.rememberAsyncImagePainter import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -51,15 +56,22 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes /** - * Square cover-image picker shared by the music track and playlist composers. When a file is - * picked it shows the upload preview (tap to swap, close button to remove); otherwise it shows the - * dashed upload placeholder. While an upload is in flight (`enabled = false`) the tap/delete - * gestures are dropped so the user can't mutate the selection mid-upload, but the preview stays - * visible so they can see what's being sent. + * Square cover-image picker shared by the music track and playlist composers. + * + * Three states, in priority order: + * - a freshly picked local file ([cover]) → upload preview (tap to swap, close button to remove); + * - otherwise an already-published cover ([existingUrl], set when editing) → the remote image with + * the same tap-to-swap / remove affordances, so editing a track/playlist shows its current art; + * - otherwise the dashed upload placeholder. + * + * While an upload is in flight (`enabled = false`) the tap/delete gestures are dropped so the user + * can't mutate the selection mid-upload, but the preview stays visible so they can see what's being + * sent. */ @Composable fun CoverImagePicker( cover: MultiOrchestrator?, + existingUrl: String?, onPick: () -> Unit, onDelete: () -> Unit, accountViewModel: AccountViewModel, @@ -67,22 +79,82 @@ fun CoverImagePicker( ctaRes: Int, hintRes: Int, ) { - if (cover != null) { - Box(modifier = if (enabled) Modifier.clickable(onClick = onPick) else Modifier) { - ShowImageUploadGallery( - list = cover, - onDelete = { if (enabled) onDelete() }, - accountViewModel = accountViewModel, + when { + cover != null -> + Box(modifier = if (enabled) Modifier.clickable(onClick = onPick) else Modifier) { + ShowImageUploadGallery( + list = cover, + onDelete = { if (enabled) onDelete() }, + accountViewModel = accountViewModel, + ) + } + + !existingUrl.isNullOrBlank() -> + ExistingCoverPreview( + url = existingUrl, + onPick = onPick, + onDelete = onDelete, + enabled = enabled, ) - } - } else { - UploadPlaceholder( - iconSymbol = MaterialSymbols.AddPhotoAlternate, - ctaRes = ctaRes, - hintRes = hintRes, - onClick = onPick, - enabled = enabled, + + else -> + UploadPlaceholder( + iconSymbol = MaterialSymbols.AddPhotoAlternate, + ctaRes = ctaRes, + hintRes = hintRes, + onClick = onPick, + enabled = enabled, + ) + } +} + +/** + * Renders the already-published cover (a remote URL) as a square tile matching the upload preview: + * tap anywhere to pick a replacement, or use the corner button to clear it. Used in edit mode so + * the composer reflects the cover the event already carries instead of showing an empty placeholder. + */ +@Composable +private fun ExistingCoverPreview( + url: String, + onPick: () -> Unit, + onDelete: () -> Unit, + enabled: Boolean, +) { + val shape = RoundedCornerShape(12.dp) + Box( + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(shape) + .let { if (enabled) it.clickable(onClick = onPick) else it }, + ) { + val painter = rememberAsyncImagePainter(model = url) + Image( + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize(), ) + if (enabled) { + Box( + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .clip(CircleShape) + .background(Color.Black.copy(alpha = 0.55f)) + .clickable(onClick = onDelete) + .padding(4.dp), + ) { + Icon( + symbol = MaterialSymbols.Close, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(20.dp), + ) + } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsTopBar.kt index faaa8c1020..bde43095a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsTopBar.kt @@ -63,8 +63,9 @@ private fun MusicPlaylistsTopNavFilterBar( onChange: (FeedDefinition) -> Unit, ) { // Same route catalog as MusicTracks (and Articles / Longs): All Follows, Your Follows, - // kind3 Follows, Around Me, Global, custom people lists, interest sets, mute list. - val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle() + // kind3 Follows, Around Me, Global, custom people lists, interest sets, mute list — plus + // "Mine" for the user's own published playlists. + val allLists by followListsModel.musicRoutes.collectAsStateWithLifecycle() FeedFilterSpinner( placeholderCode = listName, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt index d1f7e79cfe..28217ecbe2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt @@ -62,10 +62,10 @@ private fun MusicTracksTopNavFilterBar( accountViewModel: AccountViewModel, onChange: (FeedDefinition) -> Unit, ) { - // Music is content-style (like Articles / Long-form), so reuse the same route catalog - // as those feeds — All Follows, Your Follows, kind3 Follows, Around Me, Global, custom - // people lists, interest sets, mute list. - val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle() + // Music is content-style (like Articles / Long-form), so reuse that route catalog — All + // Follows, Your Follows, kind3 Follows, Around Me, Global, custom people lists, interest + // sets, mute list — plus "Mine" for the user's own published tracks. + val allLists by followListsModel.musicRoutes.collectAsStateWithLifecycle() FeedFilterSpinner( placeholderCode = listName, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt index 4af705e568..62d842f94e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt @@ -153,6 +153,7 @@ fun NewMusicPlaylistScreen( CoverImagePicker( cover = vm.coverMedia.value, + existingUrl = vm.coverUrl.value, onPick = { wantsToPickCover = true }, onDelete = { vm.clearPickedCover() }, accountViewModel = accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt index bfa05cae50..13da1f50d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt @@ -169,6 +169,7 @@ fun NewMusicTrackScreen( CoverImagePicker( cover = vm.coverMedia.value, + existingUrl = vm.coverUrl.value, onPick = { wantsToPickCover = true }, onDelete = { vm.clearPickedCover() }, accountViewModel = accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt index 1fea16f28b..c7db6e6e21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt @@ -155,6 +155,9 @@ class NewMusicTrackViewModel : ViewModel() { fun clearPickedCover() { coverMedia.value = null + // Also drop the already-published cover URL so the remove gesture removes the existing + // artwork on save (MusicTrackEvent.edit treats a blank image as "remove the tag"). + coverUrl.value = "" } fun setPickedAudio( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt index 9417ff2782..1789d09fef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt @@ -54,15 +54,32 @@ class MusicPlaylistsFeedFilter( override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff() override fun feed(): List { - val params = buildFilterParams(account) val notes = - LocalCache.addressables.filterIntoSet(MusicPlaylistEvent.KIND) { _, it -> - accept(it, params) + if (followList() == TopFilter.Mine) { + val me = account.userProfile().pubkeyHex + LocalCache.addressables.filterIntoSet(MusicPlaylistEvent.KIND) { _, it -> isMine(it, me) } + } else { + val params = buildFilterParams(account) + LocalCache.addressables.filterIntoSet(MusicPlaylistEvent.KIND) { _, it -> accept(it, params) } } return sort(notes) } - override fun applyFilter(newItems: Set): Set = innerApplyFilter(newItems) + override fun applyFilter(newItems: Set): Set { + if (followList() == TopFilter.Mine) { + val me = account.userProfile().pubkeyHex + return newItems.filterTo(HashSet()) { isMine(it, me) } + } + return innerApplyFilter(newItems) + } + + private fun isMine( + note: Note, + me: String, + ): Boolean { + val noteEvent = note.event + return noteEvent is MusicPlaylistEvent && noteEvent.pubKey == me + } fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt index 5c1fdffee7..b11517cffc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt @@ -55,15 +55,32 @@ class MusicTracksFeedFilter( override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff() override fun feed(): List { - val params = buildFilterParams(account) val notes = - LocalCache.addressables.filterIntoSet(MusicTrackEvent.KIND) { _, it -> - accept(it, params) + if (followList() == TopFilter.Mine) { + val me = account.userProfile().pubkeyHex + LocalCache.addressables.filterIntoSet(MusicTrackEvent.KIND) { _, it -> isMine(it, me) } + } else { + val params = buildFilterParams(account) + LocalCache.addressables.filterIntoSet(MusicTrackEvent.KIND) { _, it -> accept(it, params) } } return sort(notes) } - override fun applyFilter(newItems: Set): Set = innerApplyFilter(newItems) + override fun applyFilter(newItems: Set): Set { + if (followList() == TopFilter.Mine) { + val me = account.userProfile().pubkeyHex + return newItems.filterTo(HashSet()) { isMine(it, me) } + } + return innerApplyFilter(newItems) + } + + private fun isMine( + note: Note, + me: String, + ): Boolean { + val noteEvent = note.event + return noteEvent is MusicTrackEvent && noteEvent.pubKey == me + } fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicPlaylistsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicPlaylistsSubAssembler.kt index fbd942aa97..ece543191c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicPlaylistsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicPlaylistsSubAssembler.kt @@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.MUSIC_PLAYLIST_KINDS +import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.filterMusicEventsMine import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -52,6 +54,12 @@ class MusicPlaylistsSubAssembler( key: MusicPlaylistsQueryState, since: SincePerRelayMap?, ): List { + // "Mine" bypasses the follow-list machinery: query the user's own playlists by author + // against their outbox relays (same pattern as badges/communities). + if (key.listName() == TopFilter.Mine) { + val outbox = key.account.outboxRelays.flow.value + return filterMusicEventsMine(key.account.userProfile().pubkeyHex, MUSIC_PLAYLIST_KINDS, outbox, since) + } val feedSettings = key.followsPerRelay() // REQ now only asks for kind 34139 (playlists), keyed to this screen's follow // list selector — no cross-feed cursor min needed. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicTracksSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicTracksSubAssembler.kt index 01ea758deb..a6d1acbcec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicTracksSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/MusicTracksSubAssembler.kt @@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.MUSIC_TRACK_KINDS +import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.filterMusicEventsMine import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -42,6 +44,12 @@ class MusicTracksSubAssembler( key: MusicTracksQueryState, since: SincePerRelayMap?, ): List { + // "Mine" bypasses the follow-list machinery: query the user's own tracks by author + // against their outbox relays (same pattern as badges/communities). + if (key.listName() == TopFilter.Mine) { + val outbox = key.account.outboxRelays.flow.value + return filterMusicEventsMine(key.account.userProfile().pubkeyHex, MUSIC_TRACK_KINDS, outbox, since) + } val feedSettings = key.followsPerRelay() // REQ now only asks for kind 36787 (tracks), so the `since` cursor lines up with // the tracks feed alone — no cross-feed min needed. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/subassemblies/FilterMusicEventsMine.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/subassemblies/FilterMusicEventsMine.kt new file mode 100644 index 0000000000..c1bb44495c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/datasource/subassemblies/FilterMusicEventsMine.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies + +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +/** + * Builds the relay filters for the "Mine" music selector: the user's own tracks/playlists, + * queried by author against their outbox relays. Mirrors `filterBadgesMine` / + * `filterCommunitiesMine`. The `kinds` list scopes it to tracks (36787) or playlists (34139). + */ +fun filterMusicEventsMine( + pubkey: HexKey, + kinds: List, + relays: Set, + since: SincePerRelayMap?, +): List { + if (relays.isEmpty() || pubkey.isEmpty()) return emptyList() + val authors = listOf(pubkey) + return relays.map { relay -> + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = kinds, + authors = authors, + limit = 200, + since = since?.get(relay)?.time, + ), + ) + } +}