Merge pull request #4059 from vitorpamplona/claude/amethyst-commons-migration-ua8ma8

Move the relay-filter subassemblies and datasource assemblers to commons/relayClient
This commit is contained in:
Vitor Pamplona
2026-09-05 17:13:19 -04:00
committed by GitHub
390 changed files with 2157 additions and 4366 deletions
@@ -221,15 +221,6 @@ import com.vitorpamplona.quartz.nip18Reposts.BaseRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip19Bech32.isATag
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
@@ -743,7 +734,9 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
fun getRelayGroupChannelIfExists(key: GroupId): RelayGroupChannel? = relayGroupChannels.get(key)
/** Every relay group we know of that is hosted on [relay] (its channel directory). */
fun getRelayGroupChannelsOnRelay(relay: NormalizedRelayUrl): List<RelayGroupChannel> = relayGroupChannels.filter { key, _ -> key.relayUrl == relay }
override fun getRelayGroupChannelsOnRelay(relay: NormalizedRelayUrl): List<RelayGroupChannel> = relayGroupChannels.filter { key, _ -> key.relayUrl == relay }
override fun allRelayGroupChannels(): List<RelayGroupChannel> = relayGroupChannels.values().toList()
/**
* The [RelayGroupChannel] a group-scoped content [note] belongs to, resolved the same way
@@ -3300,53 +3293,8 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
wasVerified: Boolean,
): Boolean = !event.isDeleted() && consumeBaseReplaceable(event, relay, wasVerified)
fun consume(nip19: Entity) {
when (nip19) {
is NSec -> {
getOrCreateUser(nip19.toPubKeyHex())
}
is NPub -> {
getOrCreateUser(nip19.hex)
}
is NProfile -> {
nip19.relay.forEach { relayHint ->
relayHints.addKey(nip19.hex, relayHint)
}
getOrCreateUser(nip19.hex)
}
is NNote -> {
getOrCreateNote(nip19.hex)
}
is NEvent -> {
nip19.relay.forEach { relayHint ->
relayHints.addEvent(nip19.hex, relayHint)
}
val note = getOrCreateNote(nip19.hex)
if (note.author == null) {
nip19.author?.let { note.author = checkGetOrCreateUser(it) }
}
}
is NEmbed -> {
justConsume(nip19.event, null, false)
}
is NRelay -> {}
is NAddress -> {
val aTag = nip19.aTag()
nip19.relay.forEach { relayHint ->
relayHints.addAddress(aTag, relayHint)
}
getOrCreateAddressableNote(nip19.address())
}
else -> { }
}
override fun consumeEmbedded(event: Event) {
justConsume(event, null, false)
}
override fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true)
@@ -20,24 +20,59 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand
import com.vitorpamplona.amethyst.commons.relayClient.articles.ArticlesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.badges.BadgesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.calendars.CalendarsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.channel.ChannelFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.chess.ChessFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.communities.CommunityFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.communities.list.CommunitiesListFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.discover.DiscoveryFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.emojipacks.BrowseEmojiSetsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.followPacks.FollowPacksFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.geohash.GeoHashFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.gitRepo.RepositoryFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.gitRepositories.GitRepositoriesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.highlights.HighlightsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.livestreams.LiveStreamsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.longs.LongsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.music.MusicPlaylistsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.music.MusicTracksFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.napplets.NappletsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nests.NestsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nsites.NsitesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.pictures.PicturesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.podcasts.MyPodcastFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.podcasts.OnePodcastFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.podcasts.PodcastEpisodesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.podcasts.PodcastsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.polls.PollsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.polls.results.PollResponsesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.products.ProductsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.profile.UserProfileFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.publicChats.PublicChatsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.relay.RelayFeedFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.relays.RelayInfoNip66FilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.shorts.ShortsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.softwareapps.SoftwareAppsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.url.UrlFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.video.VideoFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.wallet.OnchainZapsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.workouts.WorkoutsFilterAssembler
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountForegroundFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblyGroup
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.apps.recommendations.datasource.ProfileAppRecommendationsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.datasource.BadgesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.profile.datasource.ProfileBadgesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource.ChatroomFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistoryFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.BuzzDmJoinedChatTailFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupJoinedChatTailFilterAssembler
@@ -49,48 +84,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayG
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsDiscoveryFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelayFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasource.BrowseEmojiSetsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.datasource.RepositoryFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.GitRepositoriesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.datasource.HighlightsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.MusicPlaylistsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.MusicTracksFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.ConnectedAppsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomLivenessAssembler
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
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.results.datasources.PollResponsesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource.ProductsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.datasource.PublicChatsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource.RelayFeedFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource.SoftwareAppsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource.UrlFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.datasource.OnchainZapsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssembler
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
@@ -154,10 +154,10 @@ class RelaySubscriptionsCoordinator(
val concordChannelHistory = ConcordChannelHistoryFilterAssembler(client)
val chatroom = ChatroomFilterAssembler(client)
val community = CommunityFilterAssembler(client)
val gitRepository = RepositoryFilterAssembler(client)
val community = CommunityFilterAssembler(cache, client)
val gitRepository = RepositoryFilterAssembler(cache, client)
val thread = ThreadFilterAssembler(client)
val profile = UserProfileFilterAssembler(client)
val profile = UserProfileFilterAssembler(cache, client)
val hashtags = HashtagFilterAssembler(client)
val geohashes = GeoHashFilterAssembler(client)
val urls = UrlFilterAssembler(client)
@@ -170,7 +170,7 @@ class RelaySubscriptionsCoordinator(
val polls = PollsFilterAssembler(client)
// Votes for the poll whose results screen is open.
val pollResponses = PollResponsesFilterAssembler(client)
val pollResponses = PollResponsesFilterAssembler(cache, client)
val pictures = PicturesFilterAssembler(client)
val workouts = WorkoutsFilterAssembler(client)
val gitRepositories = GitRepositoriesFilterAssembler(client)
@@ -189,8 +189,8 @@ class RelaySubscriptionsCoordinator(
val musicPlaylists = MusicPlaylistsFilterAssembler(client)
val podcastEpisodes = PodcastEpisodesFilterAssembler(client)
val podcasts = PodcastsFilterAssembler(client)
val onePodcast = OnePodcastFilterAssembler(client)
val myPodcast = MyPodcastFilterAssembler(client)
val onePodcast = OnePodcastFilterAssembler(cache, client)
val myPodcast = MyPodcastFilterAssembler(cache, client)
val softwareApps = SoftwareAppsFilterAssembler(client)
val napplets = NappletsFilterAssembler(client)
val connectedApps = ConnectedAppsFilterAssembler(client)
@@ -205,7 +205,7 @@ class RelaySubscriptionsCoordinator(
val nwc = NWCPaymentFilterAssembler(client)
// active when the wallet's on-chain transactions screen is on top.
val onchainZaps = OnchainZapsFilterAssembler(client)
val onchainZaps = OnchainZapsFilterAssembler(cache, client)
// active while the user is browsing the NIP-87 mint picker. Subscribes to
// kind:38172 cashu mint announcements + kind:38000 cashu-scoped
@@ -21,32 +21,19 @@
package com.vitorpamplona.amethyst.service.relayClient.searchCommand
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableQueryState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchPostWatcherSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchUserWatcherSubAssembler
import com.vitorpamplona.amethyst.commons.relayClient.search.SearchPostWatcherSubAssembler
import com.vitorpamplona.amethyst.commons.relayClient.search.SearchQueryState
import com.vitorpamplona.amethyst.commons.relayClient.search.SearchUserWatcherSubAssembler
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@Stable
class SearchQueryState(
val searchQuery: MutableStateFlow<String>,
override val account: Account,
) : MutableQueryState,
AccountScopedQuery {
override fun flow(): Flow<String> = searchQuery
}
@Stable
class SearchFilterAssembler(
client: INostrClient,
scope: CoroutineScope,
val cache: LocalCache,
val cache: ICacheProvider,
) : MutableComposeSubscriptionManager<SearchQueryState>(scope) {
val group =
listOf(
@@ -25,9 +25,9 @@ import androidx.compose.runtime.Stable
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.relayClient.search.SearchQueryState
import com.vitorpamplona.amethyst.logTime
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull
@@ -92,7 +92,14 @@ class UserSuggestionState(
) {
val invalidations = MutableStateFlow(0)
val currentWord = MutableStateFlow("")
val searchDataSourceState = SearchQueryState(MutableStateFlow(""), account)
val searchDataSourceState =
SearchQueryState(
searchQuery = MutableStateFlow(""),
account = account,
searchRelays = account.searchRelayList.flow,
indexerRelays = account.indexerRelayList.flow,
followPlusAllMineWithSearchRelays = account.followPlusAllMineWithSearch.flow,
)
@OptIn(FlowPreview::class)
val searchTerm =
@@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList
import com.vitorpamplona.amethyst.commons.relayClient.communities.CommunityFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.geo_post_change_place
import com.vitorpamplona.amethyst.commons.resources.geo_post_posting_to
@@ -115,7 +116,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
@@ -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
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.topNavFeeds.TopNavFeedQueryState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import kotlinx.coroutines.flow.StateFlow
/**
* Builds the key for a top-nav-driven feed subscription out of this view model's account and feed
* states: the selected list, its per-relay resolution, and the feed(s) whose floor bounds `since`.
*/
fun AccountViewModel.topNavFeedQueryState(
listName: StateFlow<TopFilter>,
followsPerRelay: StateFlow<IFeedTopNavPerRelayFilterSet>,
vararg feeds: FeedContentState,
) = TopNavFeedQueryState(account, listName, followsPerRelay, viewModelScope, feeds.toList())
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.articles.ArticlesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun ArticlesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun ArticlesFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
ArticlesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultArticlesFollowList,
account.liveArticlesFollowListsPerRelay,
accountViewModel.feedStates.articlesFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,98 +0,0 @@
/*
* 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.articles.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.makeLongFormFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class ArticlesSubAssembler(
client: INostrClient,
allKeys: () -> Set<ArticlesQueryState>,
) : PerUserAndFollowListEoseManager<ArticlesQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: ArticlesQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeLongFormFilter(feedSettings, since, key.feedStates.articlesFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: ArticlesQueryState) = key.account.userProfile()
override fun list(key: ArticlesQueryState) = key.listName()
fun ArticlesQueryState.listNameFlow() = account.settings.defaultArticlesFollowList
fun ArticlesQueryState.listName() = listNameFlow().value
fun ArticlesQueryState.followsPerRelayFlow() = account.liveArticlesFollowListsPerRelay
fun ArticlesQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: ArticlesQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.articlesFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.badges.BadgesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun BadgesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun BadgesFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
BadgesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultBadgesFollowList,
account.liveBadgesFollowListsPerRelay,
accountViewModel.feedStates.badgesFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,99 +0,0 @@
/*
* 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.badges.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class BadgesSubAssembler(
client: INostrClient,
allKeys: () -> Set<BadgesQueryState>,
) : PerUserAndFollowListEoseManager<BadgesQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: BadgesQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author
// filter scoped to the user, so followsPerRelay() already carries authors=[me].
val defaultSince = key.feedStates.badgesFeed.lastNoteCreatedAtIfFilled()
return makeBadgesFilter(key.followsPerRelay(), since, defaultSince)
}
override fun user(key: BadgesQueryState) = key.account.userProfile()
override fun list(key: BadgesQueryState) = key.listName()
fun BadgesQueryState.listNameFlow() = account.settings.defaultBadgesFollowList
fun BadgesQueryState.listName() = listNameFlow().value
fun BadgesQueryState.followsPerRelayFlow() = account.liveBadgesFollowListsPerRelay
fun BadgesQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: BadgesQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.badgesFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -25,6 +25,7 @@ import androidx.compose.runtime.LaunchedEffect
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvites
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmChannels
import com.vitorpamplona.amethyst.commons.model.buzz.ChannelClassification
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RELAY_GROUP_METADATA_KINDS
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.buzz.buzzChannelTypes
@@ -32,7 +33,6 @@ import com.vitorpamplona.amethyst.model.buzz.classifyBuzzChannel
import com.vitorpamplona.amethyst.model.buzz.membershipNoticeFilter
import com.vitorpamplona.amethyst.model.buzz.membershipNotices
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RELAY_GROUP_METADATA_KINDS
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAllWithHooks
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -28,12 +28,12 @@ import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmChannels
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect
import com.vitorpamplona.amethyst.commons.model.cache.filter
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RELAY_GROUP_METADATA_KINDS
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.buzz.membershipNoticeFilter
import com.vitorpamplona.amethyst.model.buzz.membershipNotices
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RELAY_GROUP_METADATA_KINDS
import com.vitorpamplona.quartz.buzz.dvDmVisibility.DmVisibilityEvent
import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
@@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RELAY_GROUP_METADATA_KINDS
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RELAY_GROUP_METADATA_KINDS
import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent
import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent
import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.calendars.CalendarsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun CalendarsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun CalendarsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
CalendarsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultCalendarsFollowList,
account.liveCalendarsFollowListsPerRelay,
accountViewModel.feedStates.calendarAppointmentsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,104 +0,0 @@
/*
* 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.calendars.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class CalendarsSubAssembler(
client: INostrClient,
allKeys: () -> Set<CalendarsQueryState>,
) : PerUserAndFollowListEoseManager<CalendarsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: CalendarsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeCalendarsFilter(feedSettings, since, key.feedStates.calendarAppointmentsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: CalendarsQueryState) = key.account.userProfile()
override fun list(key: CalendarsQueryState) = key.listName()
fun CalendarsQueryState.listNameFlow() = account.settings.defaultCalendarsFollowList
fun CalendarsQueryState.listName() = listNameFlow().value
fun CalendarsQueryState.followsPerRelayFlow() = account.liveCalendarsFollowListsPerRelay
fun CalendarsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: CalendarsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
// Calendar events are addressables stored in LocalCache's WeakReference
// map: while the user is viewing list B the strong refs from list A's UI
// are gone and the GC may reclaim those notes. If the EOSE cursor for the
// list the user is switching back to still says "you have everything up
// to T", the relay won't re-send the now-evicted events. Clearing the
// cursor here forces a fresh fetch so the feed comes back whole.
clearEoseFor(key)
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.calendarAppointmentsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -28,6 +28,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.relayClient.channel.ChannelFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.channel.ChannelQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -25,6 +25,9 @@ import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry
import com.vitorpamplona.amethyst.commons.model.privateChats.DmHistoryTuning
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.account.nip01Notifications.filterGroupNotificationsToPubkey
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupAuxFilter
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupJoinedChatTailFilter
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupPreviewFilter
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.paging.WindowLoadTracker
@@ -22,13 +22,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.channel.filterRelayGroupState
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RELAY_GROUP_CARD_WARMUP_KINDS
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.isRelayGroupJoined
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.filterRelayGroupState
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
@@ -29,6 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmChannels
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupJoinedChatTailFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -25,6 +25,10 @@ import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
import com.vitorpamplona.amethyst.commons.model.privateChats.DmHistoryTuning
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.account.nip01Notifications.filterGroupNotificationsToPubkey
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupAuxFilter
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupJoinedChatTailFilter
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupLiveStateFilter
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupPreviewFilter
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.paging.WindowLoadTracker
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupStateFilters
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupHistoryFilters
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.paging.BackwardRelayPager
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay
import com.vitorpamplona.amethyst.commons.model.privateChats.DmHistoryTuning
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupAuxFilter
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupOpenChatTailFilter
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.paging.WindowLoadTracker
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupThreadsFilter
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupThreadsHistoryFilters
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.paging.BackwardRelayPager
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
@@ -30,32 +31,33 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.relay.RelayTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByAllCommunities
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByAuthors
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByCommunity
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByFollows
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByGeohashes
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByHashtag
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByMutedAuthors
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByRelay
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsGlobal
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByAllCommunities
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByAuthors
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByCommunity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByFollows
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByGeohashes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByHashtag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByMutedAuthors
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByRelay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsGlobal
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
fun filterRelayGroupsDiscovery(
cache: ICacheProvider,
feedSettings: IFeedTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> =
when (feedSettings) {
is AllCommunitiesTopNavPerRelayFilterSet -> filterRelayGroupsByAllCommunities(feedSettings, since, defaultSince)
is AllFollowsTopNavPerRelayFilterSet -> filterRelayGroupsByFollows(feedSettings, since, defaultSince)
is AuthorsTopNavPerRelayFilterSet -> filterRelayGroupsByAuthors(feedSettings, since, defaultSince)
is AllFollowsTopNavPerRelayFilterSet -> filterRelayGroupsByFollows(cache, feedSettings, since, defaultSince)
is AuthorsTopNavPerRelayFilterSet -> filterRelayGroupsByAuthors(cache, feedSettings, since, defaultSince)
is GlobalTopNavPerRelayFilterSet -> filterRelayGroupsGlobal(feedSettings, since, defaultSince)
is HashtagTopNavPerRelayFilterSet -> filterRelayGroupsByHashtag(feedSettings, since, defaultSince)
is LocationTopNavPerRelayFilterSet -> filterRelayGroupsByGeohashes(feedSettings, since, defaultSince)
is MutedAuthorsTopNavPerRelayFilterSet -> filterRelayGroupsByMutedAuthors(feedSettings, since, defaultSince)
is MutedAuthorsTopNavPerRelayFilterSet -> filterRelayGroupsByMutedAuthors(cache, feedSettings, since, defaultSince)
is RelayTopNavPerRelayFilterSet -> filterRelayGroupsByRelay(feedSettings, since, defaultSince)
is SingleCommunityTopNavPerRelayFilterSet -> filterRelayGroupsByCommunity(feedSettings, since, defaultSince)
else -> emptyList()
@@ -24,12 +24,12 @@ import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByAuthors
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.relayGroupChannelsByRelay
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.filterRelayGroupsByAuthors
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies.relayGroupChannelsByRelay
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -56,7 +56,7 @@ class RelayGroupsDiscoverySubAssembler(
val feedSettings = key.followsPerRelay()
val defaultSince = key.feedStates.relayGroupsDiscoveryFeed.lastNoteCreatedAtIfFilled()
val base = filterRelayGroupsDiscovery(feedSettings, since, defaultSince)
val base = filterRelayGroupsDiscovery(LocalCache, feedSettings, since, defaultSince)
// The follow-list filter sets resolve their relays via the outbox model (a follow's own
// publish relays), but a NIP-29 roster (39001/39002) lives ONLY on the group's host relay.
@@ -78,7 +78,7 @@ class RelayGroupsDiscoverySubAssembler(
val hostRelays = key.groupHostRelays() - alreadyQueried
if (hostRelays.isEmpty()) return base
val channelsByRelay = relayGroupChannelsByRelay()
val channelsByRelay = relayGroupChannelsByRelay(LocalCache)
val extra =
hostRelays.flatMap { relay ->
filterRelayGroupsByAuthors(
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupDirectoryFilter
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
@@ -25,6 +25,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.relayClient.chess.ChessQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelNew
@@ -44,6 +44,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.relayClient.communities.CommunityFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.feed
import com.vitorpamplona.amethyst.commons.resources.mod_queue
@@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal.CommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal.CommunityModerationFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasourc
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.communities.list.CommunitiesListFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun CommunitiesListFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun CommunitiesListFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
CommunitiesListQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultCommunitiesFollowList,
account.liveCommunitiesFollowListsPerRelay,
accountViewModel.feedStates.communitiesList,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,100 +0,0 @@
/*
* 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.communities.list.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.makeCommunitiesFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class CommunitiesListSubAssembler(
client: INostrClient,
allKeys: () -> Set<CommunitiesListQueryState>,
) : PerUserAndFollowListEoseManager<CommunitiesListQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: CommunitiesListQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author
// filter scoped to the user, so followsPerRelay() already carries authors=[me].
val defaultSince = key.feedStates.communitiesList.lastNoteCreatedAtIfFilled()
return makeCommunitiesFilter(key.followsPerRelay(), since, defaultSince)
}
override fun user(key: CommunitiesListQueryState) = key.account.userProfile()
override fun list(key: CommunitiesListQueryState) = key.listName()
fun CommunitiesListQueryState.listNameFlow() = account.settings.defaultCommunitiesFollowList
fun CommunitiesListQueryState.listName() = listNameFlow().value
fun CommunitiesListQueryState.followsPerRelayFlow() = account.liveCommunitiesFollowListsPerRelay
fun CommunitiesListQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: CommunitiesListQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.communitiesList.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.discover.DiscoveryFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.discover.DiscoveryQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -43,7 +45,21 @@ fun DiscoveryFilterAssemblerSubscription(
// even if they are tracking the same tag.
val state =
remember(accountViewModel.account) {
DiscoveryQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
val feeds = accountViewModel.feedStates
DiscoveryQueryState(
account = account,
listName = account.settings.defaultDiscoveryFollowList,
followsPerRelay = account.liveDiscoveryFollowListsPerRelay,
scope = accountViewModel.viewModelScope,
reads = feeds.discoverReads,
marketplace = feeds.discoverMarketplace,
dvms = feeds.discoverDVMs,
followSets = feeds.discoverFollowSets,
live = feeds.discoverLive,
publicChats = feeds.discoverPublicChats,
communities = feeds.discoverCommunities,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,106 +0,0 @@
/*
* 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.discover.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.makeFollowSetsFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.makeLiveActivitiesFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class DiscoveryFollowsSetsAndLiveStreamsSubAssembler2(
client: INostrClient,
allKeys: () -> Set<DiscoveryQueryState>,
) : PerUserAndFollowListEoseManager<DiscoveryQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: DiscoveryQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeFollowSetsFilter(feedSettings, since, key.feedStates.discoverFollowSets.lastNoteCreatedAtIfFilled()) +
makeLiveActivitiesFilter(feedSettings, since, key.feedStates.discoverLive.lastNoteCreatedAtIfFilled())
}
override fun user(key: DiscoveryQueryState) = key.account.userProfile()
override fun list(key: DiscoveryQueryState) = key.listName()
fun DiscoveryQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList
fun DiscoveryQueryState.listName() = listNameFlow().value
fun DiscoveryQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay
fun DiscoveryQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: DiscoveryQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(1000).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
combine(
key.feedStates.discoverFollowSets.lastNoteCreatedAtWhenFullyLoaded,
key.feedStates.discoverLive.lastNoteCreatedAtWhenFullyLoaded,
) {
Any()
}.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -1,109 +0,0 @@
/*
* 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.discover.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.makeLongFormFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.makeContentDVMsFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.makeClassifiedsFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class DiscoveryLongFormClassifiedsAndDVMSubAssembler1(
client: INostrClient,
allKeys: () -> Set<DiscoveryQueryState>,
) : PerUserAndFollowListEoseManager<DiscoveryQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: DiscoveryQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeLongFormFilter(feedSettings, since, key.feedStates.discoverReads.lastNoteCreatedAtIfFilled()) +
makeClassifiedsFilter(feedSettings, since, key.feedStates.discoverMarketplace.lastNoteCreatedAtIfFilled()) +
makeContentDVMsFilter(feedSettings, since, key.feedStates.discoverDVMs.lastNoteCreatedAtIfFilled())
}
override fun user(key: DiscoveryQueryState) = key.account.userProfile()
override fun list(key: DiscoveryQueryState) = key.listName()
fun DiscoveryQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList
fun DiscoveryQueryState.listName() = listNameFlow().value
fun DiscoveryQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay
fun DiscoveryQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: DiscoveryQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
combine(
key.feedStates.discoverReads.lastNoteCreatedAtWhenFullyLoaded,
key.feedStates.discoverDVMs.lastNoteCreatedAtWhenFullyLoaded,
key.feedStates.discoverMarketplace.lastNoteCreatedAtWhenFullyLoaded,
) {
Any()
}.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -1,105 +0,0 @@
/*
* 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.discover.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.makePublicChatsFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.makeCommunitiesFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class DiscoveryPublicChatsAndCommunitiesSubAssembler3(
client: INostrClient,
allKeys: () -> Set<DiscoveryQueryState>,
) : PerUserAndFollowListEoseManager<DiscoveryQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: DiscoveryQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makePublicChatsFilter(feedSettings, since, key.feedStates.discoverPublicChats.lastNoteCreatedAtIfFilled()) +
makeCommunitiesFilter(feedSettings, since, key.feedStates.discoverCommunities.lastNoteCreatedAtIfFilled())
}
override fun user(key: DiscoveryQueryState) = key.account.userProfile()
override fun list(key: DiscoveryQueryState) = key.listName()
fun DiscoveryQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList
fun DiscoveryQueryState.listName() = listNameFlow().value
fun DiscoveryQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay
fun DiscoveryQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: DiscoveryQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
combine(
key.feedStates.discoverPublicChats.lastNoteCreatedAtWhenFullyLoaded,
key.feedStates.discoverCommunities.lastNoteCreatedAtWhenFullyLoaded,
) {
Any()
}.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasour
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.emojipacks.BrowseEmojiSetsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun BrowseEmojiSetsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun BrowseEmojiSetsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
BrowseEmojiSetsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultBrowseEmojiSetsFollowList,
account.liveBrowseEmojiSetsFollowListsPerRelay,
accountViewModel.feedStates.browseEmojiSetsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,98 +0,0 @@
/*
* 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.emojipacks.browse.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class BrowseEmojiSetsSubAssembler(
client: INostrClient,
allKeys: () -> Set<BrowseEmojiSetsQueryState>,
) : PerUserAndFollowListEoseManager<BrowseEmojiSetsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: BrowseEmojiSetsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
val defaultSince = key.feedStates.browseEmojiSetsFeed.lastNoteCreatedAtIfFilled()
return makeBrowseEmojiSetsFilter(feedSettings, since, defaultSince)
}
override fun user(key: BrowseEmojiSetsQueryState) = key.account.userProfile()
override fun list(key: BrowseEmojiSetsQueryState) = key.listName()
fun BrowseEmojiSetsQueryState.listNameFlow() = account.settings.defaultBrowseEmojiSetsFollowList
fun BrowseEmojiSetsQueryState.listName() = listNameFlow().value
fun BrowseEmojiSetsQueryState.followsPerRelayFlow() = account.liveBrowseEmojiSetsFollowListsPerRelay
fun BrowseEmojiSetsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: BrowseEmojiSetsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.feedStates.browseEmojiSetsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasourc
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.followPacks.FollowPacksFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun FollowPacksFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun FollowPacksFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
FollowPacksQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultFollowPacksFollowList,
account.liveFollowPacksFollowListsPerRelay,
accountViewModel.feedStates.followPacksFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,98 +0,0 @@
/*
* 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.followPacks.list.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.makeFollowSetsFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class FollowPacksSubAssembler(
client: INostrClient,
allKeys: () -> Set<FollowPacksQueryState>,
) : PerUserAndFollowListEoseManager<FollowPacksQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: FollowPacksQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeFollowSetsFilter(feedSettings, since, key.feedStates.followPacksFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: FollowPacksQueryState) = key.account.userProfile()
override fun list(key: FollowPacksQueryState) = key.listName()
fun FollowPacksQueryState.listNameFlow() = account.settings.defaultFollowPacksFollowList
fun FollowPacksQueryState.listName() = listNameFlow().value
fun FollowPacksQueryState.followsPerRelayFlow() = account.liveFollowPacksFollowListsPerRelay
fun FollowPacksQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: FollowPacksQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.followPacksFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.geohash.GeohashQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.nip34Git.GitBrowseState
import com.vitorpamplona.amethyst.commons.nip34Git.GitRepoSnapshotCache
import com.vitorpamplona.amethyst.commons.nip34Git.GitRepositoryBrowserViewModel
import com.vitorpamplona.amethyst.commons.relayClient.gitRepo.RepositoryFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.git_new_issue_button
import com.vitorpamplona.amethyst.commons.resources.git_repo_bookmark
@@ -96,7 +97,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.code.GitCodeTab
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.code.GitReadmeSection
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.dal.RepositoryIssuesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.dal.RepositoryPatchesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.datasource.RepositoryFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.gitRepositories.GitRepositoriesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun GitRepositoriesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun GitRepositoriesFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
GitRepositoriesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultGitRepositoriesFollowList,
account.liveGitRepositoriesFollowListsPerRelay,
accountViewModel.feedStates.gitRepositoriesFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,100 +0,0 @@
/*
* 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.gitRepositories.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class GitRepositoriesSubAssembler(
client: INostrClient,
allKeys: () -> Set<GitRepositoriesQueryState>,
) : PerUserAndFollowListEoseManager<GitRepositoriesQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: GitRepositoriesQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author
// filter scoped to the user, so followsPerRelay() already carries authors=[me] against the
// user's own outbox.
val feedSettings = key.followsPerRelay()
return makeGitRepositoriesFilter(feedSettings, since, key.feedStates.gitRepositoriesFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: GitRepositoriesQueryState) = key.account.userProfile()
override fun list(key: GitRepositoriesQueryState) = key.listName()
fun GitRepositoriesQueryState.listNameFlow() = account.settings.defaultGitRepositoriesFollowList
fun GitRepositoriesQueryState.listName() = listNameFlow().value
fun GitRepositoriesQueryState.followsPerRelayFlow() = account.liveGitRepositoriesFollowListsPerRelay
fun GitRepositoriesQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: GitRepositoriesQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.gitRepositoriesFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -20,10 +20,10 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource
import com.vitorpamplona.amethyst.commons.relayClient.nip22Comments.CommentKinds
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.CommentKinds
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.highlights.HighlightsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun HighlightsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun HighlightsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
HighlightsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultHighlightsFollowList,
account.liveHighlightsFollowListsPerRelay,
accountViewModel.feedStates.highlightsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,100 +0,0 @@
/*
* 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.highlights.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class HighlightsSubAssembler(
client: INostrClient,
allKeys: () -> Set<HighlightsQueryState>,
) : PerUserAndFollowListEoseManager<HighlightsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: HighlightsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// "Mine" needs no special-case: the shared TopFilter.Mine flow resolves to an author
// filter scoped to the user, so followsPerRelay() already carries authors=[me] against the
// user's own outbox.
val feedSettings = key.followsPerRelay()
return makeHighlightsFilter(feedSettings, since, key.feedStates.highlightsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: HighlightsQueryState) = key.account.userProfile()
override fun list(key: HighlightsQueryState) = key.listName()
fun HighlightsQueryState.listNameFlow() = account.settings.defaultHighlightsFollowList
fun HighlightsQueryState.listName() = listNameFlow().value
fun HighlightsQueryState.followsPerRelayFlow() = account.liveHighlightsFollowListsPerRelay
fun HighlightsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: HighlightsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.highlightsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -21,10 +21,10 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.nip22Comments.filterHomePostsByScopes
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -21,10 +21,10 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.nip22Comments.filterHomePostsByScopes
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
@@ -21,10 +21,10 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.nip22Comments.filterHomePostsByScopes
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByGeohashes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByHashtags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsFromAllCommunities
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.livestreams.LiveStreamsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun LiveStreamsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun LiveStreamsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
LiveStreamsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultLiveStreamsFollowList,
account.liveLiveStreamsFollowListsPerRelay,
accountViewModel.feedStates.liveStreamsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,98 +0,0 @@
/*
* 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.livestreams.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.makeLiveActivitiesFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class LiveStreamsSubAssembler(
client: INostrClient,
allKeys: () -> Set<LiveStreamsQueryState>,
) : PerUserAndFollowListEoseManager<LiveStreamsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: LiveStreamsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeLiveActivitiesFilter(feedSettings, since, key.feedStates.liveStreamsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: LiveStreamsQueryState) = key.account.userProfile()
override fun list(key: LiveStreamsQueryState) = key.listName()
fun LiveStreamsQueryState.listNameFlow() = account.settings.defaultLiveStreamsFollowList
fun LiveStreamsQueryState.listName() = listNameFlow().value
fun LiveStreamsQueryState.followsPerRelayFlow() = account.liveLiveStreamsFollowListsPerRelay
fun LiveStreamsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: LiveStreamsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.liveStreamsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
@@ -31,7 +32,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.SupportedContent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoMeta
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.longs.LongsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun LongsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun LongsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
LongsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultLongsFollowList,
account.liveLongsFollowListsPerRelay,
accountViewModel.feedStates.longsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,97 +0,0 @@
/*
* 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.longs.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class LongsSubAssembler(
client: INostrClient,
allKeys: () -> Set<LongsQueryState>,
) : PerUserAndFollowListEoseManager<LongsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: LongsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeLongsFilter(feedSettings, since, key.feedStates.longsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: LongsQueryState) = key.account.userProfile()
override fun list(key: LongsQueryState) = key.listName()
fun LongsQueryState.listNameFlow() = account.settings.defaultLongsFollowList
fun LongsQueryState.listName() = listNameFlow().value
fun LongsQueryState.followsPerRelayFlow() = account.liveLongsFollowListsPerRelay
fun LongsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: LongsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.longsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.music.MusicPlaylistsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun MusicPlaylistsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun MusicPlaylistsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
MusicPlaylistsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultMusicPlaylistsFollowList,
account.liveMusicPlaylistsFollowListsPerRelay,
accountViewModel.feedStates.musicPlaylistsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,110 +0,0 @@
/*
* 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
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
/**
* Mirror of [MusicTracksSubAssembler] but keyed on the user's *playlists* follow list
* ([com.vitorpamplona.amethyst.model.AccountSettings.defaultMusicPlaylistsFollowList]).
* The playlists screen exposes its own top-bar spinner, and the previous setup reused the
* tracks subscription — so if the user picked, e.g., Global for playlists but Following for
* tracks, the REQ only asked Following's authors and the playlists feed showed empty.
*
* Asks for kind 34139 only — tracks referenced by playlists are loaded on demand by each
* playlist row's own per-track observer (see MusicPlaylist.kt), not bundled into this REQ.
*/
class MusicPlaylistsSubAssembler(
client: INostrClient,
allKeys: () -> Set<MusicPlaylistsQueryState>,
) : PerUserAndFollowListEoseManager<MusicPlaylistsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: MusicPlaylistsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author
// filter scoped to the user, so followsPerRelay() already carries authors=[me].
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.
return makeMusicPlaylistsFilter(feedSettings, since, key.feedStates.musicPlaylistsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: MusicPlaylistsQueryState) = key.account.userProfile()
override fun list(key: MusicPlaylistsQueryState) = key.listName()
fun MusicPlaylistsQueryState.listNameFlow() = account.settings.defaultMusicPlaylistsFollowList
fun MusicPlaylistsQueryState.listName() = listNameFlow().value
fun MusicPlaylistsQueryState.followsPerRelayFlow() = account.liveMusicPlaylistsFollowListsPerRelay
fun MusicPlaylistsQueryState.followsPerRelay() = followsPerRelayFlow().value
private val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: MusicPlaylistsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.musicPlaylistsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.music.MusicTracksFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun MusicTracksFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun MusicTracksFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
MusicTracksQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultMusicTracksFollowList,
account.liveMusicTracksFollowListsPerRelay,
accountViewModel.feedStates.musicTracksFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,100 +0,0 @@
/*
* 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
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class MusicTracksSubAssembler(
client: INostrClient,
allKeys: () -> Set<MusicTracksQueryState>,
) : PerUserAndFollowListEoseManager<MusicTracksQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: MusicTracksQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author
// filter scoped to the user, so followsPerRelay() already carries authors=[me].
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.
return makeMusicTracksFilter(feedSettings, since, key.feedStates.musicTracksFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: MusicTracksQueryState) = key.account.userProfile()
override fun list(key: MusicTracksQueryState) = key.listName()
fun MusicTracksQueryState.listNameFlow() = account.settings.defaultMusicTracksFollowList
fun MusicTracksQueryState.listName() = listNameFlow().value
fun MusicTracksQueryState.followsPerRelayFlow() = account.liveMusicTracksFollowListsPerRelay
fun MusicTracksQueryState.followsPerRelay() = followsPerRelayFlow().value
private val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: MusicTracksQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.musicTracksFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.napplets.NappletsFilterAssembler
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
@@ -21,8 +21,8 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.napplets.filterNappletsByAuthors
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.subassemblies.filterNappletsByAuthors
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -1,62 +0,0 @@
/*
* 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.napplets.datasource
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import kotlinx.coroutines.CoroutineScope
/**
* Keyspace for the napplet-discovery subscription. Unlike a feed datasource it carries no
* `FeedContentState` — [com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen]
* reads the manifests straight out of `LocalCache`, so this only needs the account (for relay
* selection) and a scope.
*/
class NappletsQueryState(
override val account: Account,
val scope: CoroutineScope,
) : AccountScopedQuery
/**
* Subscribes to NIP-5D napplet manifests (kinds 15129/35129) while a napplet screen is open,
* honoring the top-nav follow-list selection at the relay level (see [NappletsFilterSubAssembler])
* and dumping the results into `LocalCache` for the screen to observe. Registered as an app-lifetime
* singleton in `RelaySubscriptionsCoordinator` (the underlying EOSE manager opens its relay
* subscription at construction, so it must not be created per screen).
*/
@Stable
class NappletsFilterAssembler(
client: INostrClient,
) : ComposeSubscriptionManager<NappletsQueryState>() {
val group =
listOf(
NappletsFilterSubAssembler(client, ::allKeys),
)
override fun invalidateKeys() = invalidateFilters()
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
override fun destroy() = group.forEach { it.destroy() }
}
@@ -22,15 +22,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun NappletsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
val state =
remember(accountViewModel.account) {
NappletsQueryState(accountViewModel.account, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultNappletsFollowList,
account.liveNappletsFollowListsPerRelay,
)
}
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().napplets)
@@ -1,100 +0,0 @@
/*
* 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.napplets.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
/**
* Subscribes to NIP-5D napplet manifests (kinds 15129/35129), honoring the top-nav follow-list
* selection at the relay level: one subscription per logged-in user + selected list, with the authors
* resolved to each author's outbox relays (via [com.vitorpamplona.amethyst.model.Account.liveNappletsFollowListsPerRelay]).
* Mirrors [com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesSubAssembler]; the
* only spinner options are author-based (see `authorOnlyRoutes`), so [makeNappletsFilter] needs no
* tag-based branches.
*/
class NappletsFilterSubAssembler(
client: INostrClient,
allKeys: () -> Set<NappletsQueryState>,
) : PerUserAndFollowListEoseManager<NappletsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: NappletsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author
// filter scoped to the user, so followsPerRelay() already carries authors=[me].
return makeNappletsFilter(key.followsPerRelay(), since)
}
override fun user(key: NappletsQueryState) = key.account.userProfile()
override fun list(key: NappletsQueryState) = key.listName()
fun NappletsQueryState.listNameFlow() = account.settings.defaultNappletsFollowList
fun NappletsQueryState.listName() = listNameFlow().value
fun NappletsQueryState.followsPerRelayFlow() = account.liveNappletsFollowListsPerRelay
fun NappletsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: NappletsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.nests.NestsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun NestsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun NestsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
NestsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultNestsFollowList,
account.liveNestsFollowListsPerRelay,
accountViewModel.feedStates.nestsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,102 +0,0 @@
/*
* 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.nests.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
/**
* Reuses `makeLiveActivitiesFilter`, which already subscribes to kinds
* 30311/30312/30313/1311. The feed filter narrows to 30312/30313 client-side;
* sharing the wire filter avoids duplicate REQs on relays when both the Live
* Streams and Audio Rooms screens are open for the same user.
*/
class NestsSubAssembler(
client: INostrClient,
allKeys: () -> Set<NestsQueryState>,
) : PerUserAndFollowListEoseManager<NestsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: NestsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeNestsFilter(feedSettings, since, key.feedStates.nestsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: NestsQueryState) = key.account.userProfile()
override fun list(key: NestsQueryState) = key.listName()
fun NestsQueryState.listNameFlow() = account.settings.defaultNestsFollowList
fun NestsQueryState.listName() = listNameFlow().value
fun NestsQueryState.followsPerRelayFlow() = account.liveNestsFollowListsPerRelay
fun NestsQueryState.followsPerRelay() = followsPerRelayFlow().value
private val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: NestsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.nestsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -1,62 +0,0 @@
/*
* 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.nsites.datasource
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import kotlinx.coroutines.CoroutineScope
/**
* Keyspace for the nSite-discovery subscription. Like its nApplet sibling it carries no
* `FeedContentState` — [com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.NsitesScreen] reads the
* manifests straight out of `LocalCache`, so this only needs the account (for relay selection) and a
* scope.
*/
class NsitesQueryState(
override val account: Account,
val scope: CoroutineScope,
) : AccountScopedQuery
/**
* Subscribes to NIP-5A static-site manifests (kinds 15128/35128) while an nSite screen is open,
* honoring the top-nav follow-list selection at the relay level (see [NsitesFilterSubAssembler]) and
* dumping the results into `LocalCache` for the screen to observe. Registered as an app-lifetime
* singleton in `RelaySubscriptionsCoordinator` (the underlying EOSE manager opens its relay
* subscription at construction, so it must not be created per screen).
*/
@Stable
class NsitesFilterAssembler(
client: INostrClient,
) : ComposeSubscriptionManager<NsitesQueryState>() {
val group =
listOf(
NsitesFilterSubAssembler(client, ::allKeys),
)
override fun invalidateKeys() = invalidateFilters()
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
override fun destroy() = group.forEach { it.destroy() }
}
@@ -22,15 +22,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun NsitesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
val state =
remember(accountViewModel.account) {
NsitesQueryState(accountViewModel.account, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultNsitesFollowList,
account.liveNsitesFollowListsPerRelay,
)
}
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().nsites)
@@ -1,100 +0,0 @@
/*
* 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.nsites.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
/**
* Subscribes to NIP-5A static-site manifests (kinds 15128/35128), honoring the top-nav follow-list
* selection at the relay level: one subscription per logged-in user + selected list, with the authors
* resolved to each author's outbox relays (via [com.vitorpamplona.amethyst.model.Account.liveNsitesFollowListsPerRelay]).
* Mirrors [com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesSubAssembler]; the
* only spinner options are author-based (see `authorOnlyRoutes`), so [makeNsitesFilter] needs no
* tag-based branches.
*/
class NsitesFilterSubAssembler(
client: INostrClient,
allKeys: () -> Set<NsitesQueryState>,
) : PerUserAndFollowListEoseManager<NsitesQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: NsitesQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// "Mine" needs no special-case: the shared TopFilter.Mine flow now resolves to an author
// filter scoped to the user, so followsPerRelay() already carries authors=[me].
return makeNsitesFilter(key.followsPerRelay(), since)
}
override fun user(key: NsitesQueryState) = key.account.userProfile()
override fun list(key: NsitesQueryState) = key.listName()
fun NsitesQueryState.listNameFlow() = account.settings.defaultNsitesFollowList
fun NsitesQueryState.listName() = listNameFlow().value
fun NsitesQueryState.followsPerRelayFlow() = account.liveNsitesFollowListsPerRelay
fun NsitesQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: NsitesQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.pictures.PicturesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun PicturesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun PicturesFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
PicturesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultPicturesFollowList,
account.livePicturesFollowListsPerRelay,
accountViewModel.feedStates.picturesFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,97 +0,0 @@
/*
* 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.pictures.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class PicturesSubAssembler(
client: INostrClient,
allKeys: () -> Set<PicturesQueryState>,
) : PerUserAndFollowListEoseManager<PicturesQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: PicturesQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makePicturesFilter(feedSettings, since, key.feedStates.picturesFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: PicturesQueryState) = key.account.userProfile()
override fun list(key: PicturesQueryState) = key.listName()
fun PicturesQueryState.listNameFlow() = account.settings.defaultPicturesFollowList
fun PicturesQueryState.listName() = listNameFlow().value
fun PicturesQueryState.followsPerRelayFlow() = account.livePicturesFollowListsPerRelay
fun PicturesQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: PicturesQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.picturesFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,6 +22,7 @@ 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.podcasts.MyPodcastQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.relayClient.podcasts.OnePodcastQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.podcasts.PodcastEpisodesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun PodcastEpisodesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun PodcastEpisodesFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
PodcastEpisodesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultPodcastEpisodesFollowList,
account.livePodcastEpisodesFollowListsPerRelay,
accountViewModel.feedStates.podcastEpisodesFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,96 +0,0 @@
/*
* 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.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class PodcastEpisodesSubAssembler(
client: INostrClient,
allKeys: () -> Set<PodcastEpisodesQueryState>,
) : PerUserAndFollowListEoseManager<PodcastEpisodesQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: PodcastEpisodesQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makePodcastEpisodesFilter(feedSettings, since, key.feedStates.podcastEpisodesFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: PodcastEpisodesQueryState) = key.account.userProfile()
override fun list(key: PodcastEpisodesQueryState) = key.listName()
fun PodcastEpisodesQueryState.listNameFlow() = account.settings.defaultPodcastEpisodesFollowList
fun PodcastEpisodesQueryState.listName() = listNameFlow().value
fun PodcastEpisodesQueryState.followsPerRelayFlow() = account.livePodcastEpisodesFollowListsPerRelay
fun PodcastEpisodesQueryState.followsPerRelay() = followsPerRelayFlow().value
private val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: PodcastEpisodesQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.podcastEpisodesFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.podcasts.PodcastsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun PodcastsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun PodcastsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
PodcastsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultPodcastsFollowList,
account.livePodcastsFollowListsPerRelay,
accountViewModel.feedStates.podcastsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,96 +0,0 @@
/*
* 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.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class PodcastsSubAssembler(
client: INostrClient,
allKeys: () -> Set<PodcastsQueryState>,
) : PerUserAndFollowListEoseManager<PodcastsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: PodcastsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makePodcastsFilter(feedSettings, since, key.feedStates.podcastsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: PodcastsQueryState) = key.account.userProfile()
override fun list(key: PodcastsQueryState) = key.listName()
fun PodcastsQueryState.listNameFlow() = account.settings.defaultPodcastsFollowList
fun PodcastsQueryState.listName() = listNameFlow().value
fun PodcastsQueryState.followsPerRelayFlow() = account.livePodcastsFollowListsPerRelay
fun PodcastsQueryState.followsPerRelay() = followsPerRelayFlow().value
private val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: PodcastsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.podcastsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.polls.PollsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun PollsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun PollsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
PollsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultPollsFollowList,
account.livePollsFollowListsPerRelay,
accountViewModel.feedStates.pollsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,97 +0,0 @@
/*
* 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.polls.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class PollsSubAssembler(
client: INostrClient,
allKeys: () -> Set<PollsQueryState>,
) : PerUserAndFollowListEoseManager<PollsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: PollsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makePollsFilter(feedSettings, since, key.feedStates.pollsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: PollsQueryState) = key.account.userProfile()
override fun list(key: PollsQueryState) = key.listName()
fun PollsQueryState.listNameFlow() = account.settings.defaultPollsFollowList
fun PollsQueryState.listName() = listNameFlow().value
fun PollsQueryState.followsPerRelayFlow() = account.livePollsFollowListsPerRelay
fun PollsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: PollsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.pollsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.results.datasources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.polls.results.PollResponsesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.polls.results.PollResponsesQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.products.ProductsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun ProductsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun ProductsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
ProductsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultProductsFollowList,
account.liveProductsFollowListsPerRelay,
accountViewModel.feedStates.productsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,98 +0,0 @@
/*
* 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.products.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.makeClassifiedsFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class ProductsSubAssembler(
client: INostrClient,
allKeys: () -> Set<ProductsQueryState>,
) : PerUserAndFollowListEoseManager<ProductsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: ProductsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeClassifiedsFilter(feedSettings, since, key.feedStates.productsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: ProductsQueryState) = key.account.userProfile()
override fun list(key: ProductsQueryState) = key.listName()
fun ProductsQueryState.listNameFlow() = account.settings.defaultProductsFollowList
fun ProductsQueryState.listName() = listNameFlow().value
fun ProductsQueryState.followsPerRelayFlow() = account.liveProductsFollowListsPerRelay
fun ProductsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: ProductsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.productsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -65,6 +65,7 @@ import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.relayClient.profile.UserProfileFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.gallery
import com.vitorpamplona.amethyst.commons.resources.mutual
@@ -86,7 +87,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.TabBookma
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.TabNotesConversations
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal.UserProfileConversationsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.FollowersTabHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.TabFollowers
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.publicChats.PublicChatsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun PublicChatsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun PublicChatsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
PublicChatsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultPublicChatsFollowList,
account.livePublicChatsFollowListsPerRelay,
accountViewModel.feedStates.publicChatsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,98 +0,0 @@
/*
* 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.publicChats.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.makePublicChatsFilter
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class PublicChatsSubAssembler(
client: INostrClient,
allKeys: () -> Set<PublicChatsQueryState>,
) : PerUserAndFollowListEoseManager<PublicChatsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: PublicChatsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makePublicChatsFilter(feedSettings, since, key.feedStates.publicChatsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: PublicChatsQueryState) = key.account.userProfile()
override fun list(key: PublicChatsQueryState) = key.listName()
fun PublicChatsQueryState.listNameFlow() = account.settings.defaultPublicChatsFollowList
fun PublicChatsQueryState.listName() = listNameFlow().value
fun PublicChatsQueryState.followsPerRelayFlow() = account.livePublicChatsFollowListsPerRelay
fun PublicChatsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: PublicChatsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.publicChatsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.relay.RelayFeedQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.relays.RelayInfoNip66QueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -32,13 +32,13 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.relayClient.search.SearchQueryState
import com.vitorpamplona.amethyst.commons.search.SearchScope
import com.vitorpamplona.amethyst.commons.search.SearchSortOrder
import com.vitorpamplona.amethyst.commons.search.SearchSource
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
@@ -104,7 +104,14 @@ class SearchBarViewModel(
.onEach(::updateDataSource)
.stateIn(viewModelScope, SharingStarted.Eagerly, searchValue)
val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account)
val searchDataSourceState =
SearchQueryState(
searchQuery = MutableStateFlow(searchValue),
account = account,
searchRelays = account.searchRelayList.flow,
indexerRelays = account.indexerRelayList.flow,
followPlusAllMineWithSearchRelays = account.followPlusAllMineWithSearch.flow,
)
@Suppress("unused")
val sourceWatcher =
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
@@ -31,7 +32,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.SupportedContent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip71Video.VideoMeta
import com.vitorpamplona.quartz.nip71Video.VideoShortEvent
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.shorts.ShortsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun ShortsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -41,7 +42,12 @@ fun ShortsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
ShortsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultShortsFollowList,
account.liveShortsFollowListsPerRelay,
accountViewModel.feedStates.shortsFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,97 +0,0 @@
/*
* 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.shorts.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class ShortsSubAssembler(
client: INostrClient,
allKeys: () -> Set<ShortsQueryState>,
) : PerUserAndFollowListEoseManager<ShortsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: ShortsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeShortsFilter(feedSettings, since, key.feedStates.shortsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: ShortsQueryState) = key.account.userProfile()
override fun list(key: ShortsQueryState) = key.listName()
fun ShortsQueryState.listNameFlow() = account.settings.defaultShortsFollowList
fun ShortsQueryState.listName() = listNameFlow().value
fun ShortsQueryState.followsPerRelayFlow() = account.liveShortsFollowListsPerRelay
fun ShortsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: ShortsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.shortsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.softwareapps.SoftwareAppsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.softwareapps.SoftwareAppsQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -41,7 +43,15 @@ fun SoftwareAppsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account) {
SoftwareAppsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
SoftwareAppsQueryState(
account = account,
listName = account.settings.defaultSoftwareAppsFollowList,
followsPerRelay = account.liveSoftwareAppsFollowListsPerRelay,
scope = accountViewModel.viewModelScope,
feed = accountViewModel.feedStates.softwareAppsFeed,
blockedRelays = account.blockedRelayList.flow,
)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
@@ -1,109 +0,0 @@
/*
* 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.softwareapps.datasource
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class SoftwareAppsSubAssembler(
client: INostrClient,
allKeys: () -> Set<SoftwareAppsQueryState>,
) : PerUserAndFollowListEoseManager<SoftwareAppsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: SoftwareAppsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeSoftwareAppsFilter(
feedSettings,
since,
key.feedStates.softwareAppsFeed.lastNoteCreatedAtIfFilled(),
key.account.blockedRelayList.flow.value,
)
}
override fun user(key: SoftwareAppsQueryState) = key.account.userProfile()
override fun list(key: SoftwareAppsQueryState) = key.listName()
fun SoftwareAppsQueryState.listNameFlow() = account.settings.defaultSoftwareAppsFollowList
fun SoftwareAppsQueryState.listName() = listNameFlow().value
fun SoftwareAppsQueryState.followsPerRelayFlow() = account.liveSoftwareAppsFollowListsPerRelay
fun SoftwareAppsQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: SoftwareAppsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
// Re-evaluate when the user blocks/unblocks zapstore's relay so the
// global Apps feed stops/starts querying it without a restart.
key.account.blockedRelayList.flow.collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.softwareAppsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.sub
import com.vitorpamplona.amethyst.commons.model.ThreadAssembler
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.thread.filterMissingEventsForThread
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState
@@ -48,7 +49,7 @@ class ThreadEventLoaderSubAssembler(
): List<RelayBasedFilter>? {
val branches = ThreadAssembler(LocalCache).findThreadFor(key.eventId) ?: return null
val defaultRelays = key.account.followPlusAllMineWithSearch.flow.value
return filterMissingEventsForThread(branches, defaultRelays)
return filterMissingEventsForThread(LocalCache, branches, defaultRelays)
}
override fun id(key: ThreadQueryState) = key.eventId
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.sub
import com.vitorpamplona.amethyst.commons.model.ThreadAssembler
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.thread.filterEventsInThreadForRoot
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState
@@ -24,6 +24,7 @@ import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.url.UrlQueryState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@SuppressLint("StateFlowValueCalledInComposition")
@@ -24,13 +24,13 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip68Picture.PictureMeta
@@ -1,51 +0,0 @@
/*
* 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.video.datasource
import com.vitorpamplona.amethyst.commons.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.VideoOutboxEventsFilterSubAssembler
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import kotlinx.coroutines.CoroutineScope
// This allows multiple screen to be listening to tags, even the same tag
class VideoQueryState(
override val account: Account,
val feedState: AccountFeedContentStates,
val scope: CoroutineScope,
) : AccountScopedQuery
class VideoFilterAssembler(
client: INostrClient,
) : ComposeSubscriptionManager<VideoQueryState>() {
val group =
listOf(
VideoOutboxEventsFilterSubAssembler(client, ::allKeys),
)
override fun invalidateKeys() = invalidateFilters()
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
override fun destroy() = group.forEach { it.destroy() }
}
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.video.VideoFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.topNavFeedQueryState
@Composable
fun VideoFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
@@ -43,7 +44,12 @@ fun VideoFilterAssemblerSubscription(
// even if they are tracking the same tag.
val state =
remember(accountViewModel.account) {
VideoQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
val account = accountViewModel.account
accountViewModel.topNavFeedQueryState(
account.settings.defaultStoriesFollowList,
account.liveStoriesFollowListsPerRelay,
accountViewModel.feedStates.videoFeed,
)
}
LifecycleAwareKeyDataSourceSubscription(state, filterAssembler)
@@ -1,119 +0,0 @@
/*
* 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.video.datasource.subassemblies
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoQueryState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoByGeohash
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoByHashtag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoGlobal
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows.filterPictureAndVideoByAuthors
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows.filterPictureAndVideoByFollows
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoByAllCommunities
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoByCommunity
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
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class VideoOutboxEventsFilterSubAssembler(
client: INostrClient,
allKeys: () -> Set<VideoQueryState>,
) : PerUserAndFollowListEoseManager<VideoQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: VideoQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
val feedSettings = key.followsPerRelay()
val defaultSince = key.feedState.videoFeed.lastNoteCreatedAtWhenFullyLoaded.value
return when (feedSettings) {
is AllCommunitiesTopNavPerRelayFilterSet -> filterPictureAndVideoByAllCommunities(feedSettings, since, defaultSince)
is AllFollowsTopNavPerRelayFilterSet -> filterPictureAndVideoByFollows(feedSettings, since, defaultSince)
is AuthorsTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since, defaultSince)
is GlobalTopNavPerRelayFilterSet -> filterPictureAndVideoGlobal(feedSettings, since, defaultSince ?: TimeUtils.oneWeekAgo())
is HashtagTopNavPerRelayFilterSet -> filterPictureAndVideoByHashtag(feedSettings, since, defaultSince ?: TimeUtils.oneMonthAgo())
is LocationTopNavPerRelayFilterSet -> filterPictureAndVideoByGeohash(feedSettings, since, defaultSince)
is MutedAuthorsTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since, defaultSince)
is SingleCommunityTopNavPerRelayFilterSet -> filterPictureAndVideoByCommunity(feedSettings, since, defaultSince)
else -> emptyList()
}
}
override fun user(key: VideoQueryState) = key.account.userProfile()
override fun list(key: VideoQueryState) = key.listName()
fun VideoQueryState.listNameFlow() = account.settings.defaultStoriesFollowList
fun VideoQueryState.listName() = listNameFlow().value
fun VideoQueryState.followsPerRelayFlow() = account.liveStoriesFollowListsPerRelay
fun VideoQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: VideoQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(1000).collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.feedState.videoFeed.lastNoteCreatedAtWhenFullyLoaded.sample(1000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}

Some files were not shown because too many files have changed in this diff Show More