mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
feat: back the podcast authoring hub with a dedicated REQ
The "Your podcast" hub previously listed only what happened to be in LocalCache, so on a fresh install (or before the feed had loaded the creator's events) it could show an empty or stale catalog. Add a MyPodcast subscription (mirrors the OnePodcast assembler trio) that, while the hub is on screen, keeps a REQ open for the creator's OWN Podcasting-2.0 catalog on their outbox relays: the addressable episodes (30054) and trailers (30055) by author, plus the show-metadata kind:30078 constrained to #d=["podcast-metadata"] (reusing the existing constant so the overloaded app-data kind isn't pulled wholesale). Registered in RelaySubscriptionsCoordinator. The hub now also reacts to LocalCache.live.newEventBundles — when the creator's own episodes/trailers/metadata arrive over the REQ, the lists refresh in place rather than only on resume. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
This commit is contained in:
+3
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomLi
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource.NsitesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.MyPodcastFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.OnePodcastFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.PodcastEpisodesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.PodcastsFilterAssembler
|
||||
@@ -144,6 +145,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val podcastEpisodes = PodcastEpisodesFilterAssembler(client)
|
||||
val podcasts = PodcastsFilterAssembler(client)
|
||||
val onePodcast = OnePodcastFilterAssembler(client)
|
||||
val myPodcast = MyPodcastFilterAssembler(client)
|
||||
val softwareApps = SoftwareAppsFilterAssembler(client)
|
||||
val napplets = NappletsFilterAssembler(client)
|
||||
val nsites = NsitesFilterAssembler(client)
|
||||
@@ -196,6 +198,7 @@ class RelaySubscriptionsCoordinator(
|
||||
podcastEpisodes,
|
||||
podcasts,
|
||||
onePodcast,
|
||||
myPodcast,
|
||||
softwareApps,
|
||||
badges,
|
||||
profileBadges,
|
||||
|
||||
+23
-1
@@ -40,6 +40,7 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -63,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.MyPodcastFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
@@ -85,12 +87,32 @@ fun PodcastAuthoringScreen(
|
||||
) {
|
||||
val me = accountViewModel.account.userProfile().pubkeyHex
|
||||
|
||||
// Bump on each resume so returning from a composer re-scans LocalCache.
|
||||
// Keep a REQ open for the creator's own catalog while the hub is visible, so the lists below
|
||||
// fill in from relays even when nothing was cached yet.
|
||||
MyPodcastFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
// Re-scan LocalCache on each resume (returning from a composer) and whenever the creator's own
|
||||
// podcast events arrive over the open REQ, so the lists stay current without a manual refresh.
|
||||
var refresh by remember { mutableIntStateOf(0) }
|
||||
LifecycleResumeEffect(Unit) {
|
||||
refresh++
|
||||
onPauseOrDispose { }
|
||||
}
|
||||
LaunchedEffect(me) {
|
||||
LocalCache.live.newEventBundles.collect { bundle ->
|
||||
val mine =
|
||||
bundle.any {
|
||||
val e = it.event
|
||||
e?.pubKey == me &&
|
||||
(
|
||||
e is Podcasting20EpisodeEvent ||
|
||||
e is Podcasting20TrailerEvent ||
|
||||
(e is AppSpecificDataEvent && e.dTag() == Podcasting20PodcastMetadata.PODCAST_METADATA_D_TAG)
|
||||
)
|
||||
}
|
||||
if (mine) refresh++
|
||||
}
|
||||
}
|
||||
|
||||
val show =
|
||||
remember(refresh) {
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.podcasts.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.subassemblies.PODCASTING20_METADATA_KINDS
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.subassemblies.PODCAST_METADATA_D_FILTER
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.subassemblies.filterPodcastEventsByAuthors
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent
|
||||
import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent
|
||||
|
||||
// What the authoring hub needs about the logged-in creator's OWN Podcasting-2.0 catalog:
|
||||
// the addressable episodes (kind 30054) and trailers (kind 30055)...
|
||||
private val MyPodcastFeedKinds = listOf(Podcasting20EpisodeEvent.KIND, Podcasting20TrailerEvent.KIND)
|
||||
|
||||
/**
|
||||
* REQ for the logged-in creator's own Podcasting-2.0 catalog, so the authoring hub reliably shows
|
||||
* their show, episodes and trailers even on a fresh install (rather than only what happens to be in
|
||||
* [LocalCache]). Two filters: the addressable episodes/trailers by author, and the show-metadata
|
||||
* `kind:30078` constrained to `#d=["podcast-metadata"]` (that kind is overloaded, so the constraint
|
||||
* keeps the REQ from pulling every app's NIP-78 data). Queried on the creator's own outbox relays.
|
||||
*/
|
||||
fun filterMyPodcast(
|
||||
user: User,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val relays =
|
||||
user.outboxRelays()?.ifEmpty { null }
|
||||
?: (user.allUsedRelays() + LocalCache.relayHints.hintsForKey(user.pubkeyHex))
|
||||
|
||||
val authors = setOf(user.pubkeyHex)
|
||||
|
||||
return relays.flatMap { relay ->
|
||||
filterPodcastEventsByAuthors(
|
||||
relay = relay,
|
||||
kinds = MyPodcastFeedKinds,
|
||||
authors = authors,
|
||||
since = since?.get(relay)?.time,
|
||||
) +
|
||||
filterPodcastEventsByAuthors(
|
||||
relay = relay,
|
||||
kinds = PODCASTING20_METADATA_KINDS,
|
||||
authors = authors,
|
||||
since = since?.get(relay)?.time,
|
||||
additionalTags = PODCAST_METADATA_D_FILTER,
|
||||
)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.podcasts.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
class MyPodcastFeedSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<MyPodcastQueryState>,
|
||||
) : PerUserEoseManager<MyPodcastQueryState>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: MyPodcastQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> = filterMyPodcast(user(key), since)
|
||||
|
||||
override fun user(key: MyPodcastQueryState) = key.user
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.podcasts.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
|
||||
// Keyed by the logged-in creator's own pubkey. Drives the authoring hub's REQ for the creator's
|
||||
// own Podcasting-2.0 catalog (episodes, trailers, and the show-metadata event).
|
||||
class MyPodcastQueryState(
|
||||
val user: User,
|
||||
)
|
||||
|
||||
class MyPodcastFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<MyPodcastQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
MyPodcastFeedSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||
|
||||
override fun destroy() = group.forEach { it.destroy() }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.podcasts.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
/**
|
||||
* While the authoring hub is on screen, keeps a REQ open for the logged-in creator's own
|
||||
* Podcasting-2.0 catalog (episodes, trailers, and show metadata), so the hub fills in even when the
|
||||
* events weren't already cached.
|
||||
*/
|
||||
@Composable
|
||||
fun MyPodcastFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
|
||||
val state =
|
||||
remember(accountViewModel) {
|
||||
MyPodcastQueryState(accountViewModel.account.userProfile())
|
||||
}
|
||||
|
||||
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().myPodcast)
|
||||
}
|
||||
Reference in New Issue
Block a user