refactor(commons): move the thread, badges, app-recommendations, connected-apps and home datasources to commons

The four families that read one relay-set state off `Account` now carry that
set as a `StateFlow` on their key, the way `SearchQueryState` does:
`ProfileAppRecommendationsQueryState(outboxRelays, defaultGlobalRelays)`,
`ProfileBadgesQueryState(notificationRelays)`,
`ConnectedAppsQueryState(homeRelays)` and `ThreadQueryState(defaultRelays)`.
Thread also takes the cache: `ThreadFilterAssembler(cache, client)`, and its
two sub-assemblers run `ThreadAssembler(cache)`.

The home feed moves whole. Its `FilterHomePosts*` dispatchers were already
import-clean; `HomeOutboxEventsEoseManager` becomes a
`TopNavFeedSubAssembler<HomeQueryState>` whose key carries the new-threads
and replies floors plus `enabledHomeFeedTypes`. The Settings › Home toggle
stays an unsampled invalidator (still `drop(1)`) via `extraInvalidators`,
and the disabled-kinds stripping is unchanged. Same deltas as the other
feeds: watchers on the screen scope, follows sampler 500 ms (was 1000 ms).
The commented-out alternative sub-assemblers in `HomeFilterAssembler` are
dropped.

The app keeps one `*Subscription.kt` per feature, building the key from
`AccountViewModel`. The algo-feed filter test moves to commons commonTest
on `kotlin.test`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J836W9ZjSUJUQ1d23TpiJo
This commit is contained in:
Claude
2026-09-05 21:32:57 +00:00
parent 15b6aace52
commit 58ada4e32e
32 changed files with 225 additions and 223 deletions
@@ -20,9 +20,11 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand
import com.vitorpamplona.amethyst.commons.relayClient.apps.recommendations.ProfileAppRecommendationsFilterAssembler
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.badges.profile.ProfileBadgesFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.calendars.CalendarsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.channel.ChannelFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.chess.ChessFilterAssembler
@@ -36,10 +38,12 @@ import com.vitorpamplona.amethyst.commons.relayClient.geohash.GeoHashFilterAssem
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.home.HomeFilterAssembler
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.ConnectedAppsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.napplets.NappletsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nests.NestsFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler
@@ -58,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.relay.RelayFeedFilterAssem
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.thread.ThreadFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.url.UrlFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.video.VideoFilterAssembler
@@ -68,8 +73,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.Account
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.badges.profile.datasource.ProfileBadgesFilterAssembler
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
@@ -86,11 +89,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayG
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.ConnectedAppsFilterAssembler
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.threadview.datasources.ThreadFilterAssembler
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
@@ -156,7 +156,7 @@ class RelaySubscriptionsCoordinator(
val chatroom = ChatroomFilterAssembler(client)
val community = CommunityFilterAssembler(cache, client)
val gitRepository = RepositoryFilterAssembler(cache, client)
val thread = ThreadFilterAssembler(client)
val thread = ThreadFilterAssembler(cache, client)
val profile = UserProfileFilterAssembler(cache, client)
val hashtags = HashtagFilterAssembler(client)
val geohashes = GeoHashFilterAssembler(client)
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.apps.recommendations.datas
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.apps.recommendations.ProfileAppRecommendationsQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -29,7 +30,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
fun ProfileAppRecommendationsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
val state =
remember(accountViewModel.account) {
ProfileAppRecommendationsQueryState(accountViewModel.account)
val account = accountViewModel.account
ProfileAppRecommendationsQueryState(
account = account,
outboxRelays = account.outboxRelays.flow,
defaultGlobalRelays = account.defaultGlobalRelays.flow,
)
}
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().profileAppRecommendations)
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.profile.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.badges.profile.ProfileBadgesQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -29,7 +30,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
fun ProfileBadgesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
val state =
remember(accountViewModel.account) {
ProfileBadgesQueryState(accountViewModel.account)
val account = accountViewModel.account
ProfileBadgesQueryState(account, account.notificationRelays.flow)
}
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().profileBadges)
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasourc
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.filterHomePostsByAuthors
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.filterHomePostsByAuthors
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
@@ -1,61 +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.home.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.amethyst.ui.screen.loggedIn.AccountFeedContentStates
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomeOutboxEventsEoseManager
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
@Stable
class HomeQueryState(
override val account: Account,
val feedState: AccountFeedContentStates,
val scope: CoroutineScope,
) : AccountScopedQuery
@Stable
class HomeFilterAssembler(
client: INostrClient,
) : ComposeSubscriptionManager<HomeQueryState>() {
val group =
listOf(
HomeOutboxEventsEoseManager(client, ::allKeys),
// HomeOutboxUsersEoseManager(client, ::allKeys),
// We can break it down, one for each sub if needed.
// HashtagEventsFilterSubAssembler(client, ::allKeys),
// GeohashEventsFilterSubAssembler(client, ::allKeys),
// CommunityEventsFilterSubAssembler(client, ::allKeys),
// MixGeohashHashtagsCommunityEoseManager(client, ::allKeys),
)
override fun invalidateKeys() = invalidateFilters()
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
override fun destroy() = group.forEach { it.destroy() }
}
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.home.HomeFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.home.HomeQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -43,10 +45,15 @@ fun HomeFilterAssemblerSubscription(
// even if they are tracking the same tag.
val state =
remember(accountViewModel.account) {
val account = accountViewModel.account
HomeQueryState(
accountViewModel.account,
accountViewModel.feedStates,
accountViewModel.viewModelScope,
account = account,
listName = account.settings.defaultHomeFollowList,
followsPerRelay = account.liveHomeFollowListsPerRelay,
scope = accountViewModel.viewModelScope,
newThreads = accountViewModel.feedStates.homeNewThreads,
replies = accountViewModel.feedStates.homeReplies,
enabledHomeFeedTypes = account.settings.enabledHomeFeedTypes,
)
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.napplets.ConnectedAppsQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -33,7 +34,8 @@ fun ConnectedAppsFilterAssemblerSubscription(
) {
val state =
remember(accountViewModel.account, authors) {
ConnectedAppsQueryState(accountViewModel.account, authors)
val account = accountViewModel.account
ConnectedAppsQueryState(account, authors, account.homeRelays.flow)
}
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().connectedApps)
@@ -24,6 +24,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.thread.ThreadFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.thread.ThreadQueryState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -48,7 +50,7 @@ fun ThreadFilterAssemblerSubscription(
// even if they are tracking the same tag.
val state =
remember(eventId) {
ThreadQueryState(eventId, account)
ThreadQueryState(eventId, account, account.followPlusAllMineWithSearch.flow)
}
LifecycleAwareKeyDataSourceSubscription(state, filterAssembler)
@@ -693,6 +693,33 @@ change each); `followPacks/feed` (dispatches to the app-side home filters);
`launchChatFeedToggleObserver`); `nests/NestRoom*`, `relayGroup/*`, `concord/*`
and `home/` (`Account` + `LocalCache` live streams + `AccountViewModel`).
### Single-relay-read families and the home feed landed (2026-09-05, after #4059)
The four families that read one relay-set state off `Account` now carry that
set as a `StateFlow` on their key, the way `SearchQueryState` does:
`ProfileAppRecommendationsQueryState(outboxRelays, defaultGlobalRelays)`,
`ProfileBadgesQueryState(notificationRelays)`,
`ConnectedAppsQueryState(homeRelays)` and `ThreadQueryState(defaultRelays)`.
Thread also takes the cache (`ThreadFilterAssembler(cache, client)`; its two
sub-assemblers run `ThreadAssembler(cache)`). All live under
`commons/relayClient/{apps/recommendations,badges/profile,napplets,thread}`.
The home feed moved whole: the `FilterHomePosts*` dispatchers under
`commons/relayClient/home/<nip>/`, and `HomeOutboxEventsEoseManager` is now a
`TopNavFeedSubAssembler<HomeQueryState>` whose key carries the new-threads and
replies floors plus `enabledHomeFeedTypes`; the Settings › Home toggle is an
`extraInvalidators` entry (still `drop(1)`), and the disabled-kinds stripping
is unchanged. Deltas as for the other feeds: watchers on the screen scope,
follows sampler 500 ms (was 1000). The commented-out alternative
sub-assemblers in `HomeFilterAssembler` were dropped.
What remains in `ui/screen/**/datasource*/` (92 files) is the Compose glue —
one `*Subscription.kt` per feature, which builds the key from
`AccountViewModel` — plus the families that need Wave 4: `hashtag/`
(`Account.followsPerRelay`), `followPacks/feed` (`Account.proxyRelayList`,
`blockedRelayList`, `cache`), `chats/*`, `nests/NestRoom*`, `relayGroup/*`,
`concord/*`, and `wallet`'s and `polls/results`' composables.
### Desktop `subscriptions/FilterBuilders.kt` — investigated, not migrated (2026-09-05)
The audit listed this 742-line file as a hand-rolled copy of ~34
@@ -18,7 +18,7 @@
* 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.apps.recommendations.datasource
package com.vitorpamplona.amethyst.commons.relayClient.apps.recommendations
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
@@ -18,16 +18,25 @@
* 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.apps.recommendations.datasource
package com.vitorpamplona.amethyst.commons.relayClient.apps.recommendations
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.IAccount
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 com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.flow.StateFlow
/**
* @param outboxRelays the user's own outbox relays, where their kind-31989 recommendations live.
* @param defaultGlobalRelays follows' outboxes plus the user's relays, where recent kind-31990
* app definitions are discovered.
*/
class ProfileAppRecommendationsQueryState(
override val account: Account,
override val account: IAccount,
val outboxRelays: StateFlow<Set<NormalizedRelayUrl>>,
val defaultGlobalRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : AccountScopedQuery
@Stable
@@ -18,7 +18,7 @@
* 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.apps.recommendations.datasource
package com.vitorpamplona.amethyst.commons.relayClient.apps.recommendations
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
@@ -37,9 +37,9 @@ class ProfileAppRecommendationsSubAssembler(
): List<RelayBasedFilter> =
filterMyAppRecommendations(
pubkey = user(key).pubkeyHex,
relays = key.account.outboxRelays.flow.value,
relays = key.outboxRelays.value,
) +
filterRecentAppDefinitions(
relays = key.account.defaultGlobalRelays.flow.value,
relays = key.defaultGlobalRelays.value,
)
}
@@ -18,7 +18,7 @@
* 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.profile.datasource
package com.vitorpamplona.amethyst.commons.relayClient.badges.profile
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
@@ -18,16 +18,20 @@
* 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.profile.datasource
package com.vitorpamplona.amethyst.commons.relayClient.badges.profile
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.IAccount
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 com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.flow.StateFlow
/** @param notificationRelays the user's inbox relays, where badge awards addressed to them land. */
class ProfileBadgesQueryState(
override val account: Account,
override val account: IAccount,
val notificationRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : AccountScopedQuery
@Stable
@@ -18,7 +18,7 @@
* 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.profile.datasource
package com.vitorpamplona.amethyst.commons.relayClient.badges.profile
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserEoseManager
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
@@ -37,6 +37,6 @@ class ProfileBadgesSubAssembler(
): List<RelayBasedFilter> =
filterReceivedBadgeAwards(
pubkey = user(key).pubkeyHex,
relays = key.account.notificationRelays.flow.value,
relays = key.notificationRelays.value,
)
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.relayClient.home
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.HomeFeedType
import com.vitorpamplona.amethyst.commons.model.IAccount
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomeOutboxEventsEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.topNavFeeds.TopNavFeedFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.topNavFeeds.TopNavFeedQueryState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
/**
* Key for the home feed: the top-nav selection, the new-threads and replies floors that bound
* `since`, and the Settings › Home content-type toggles whose disabled groups are stripped from
* every home REQ.
*/
@Stable
class HomeQueryState(
account: IAccount,
listName: StateFlow<TopFilter>,
followsPerRelay: StateFlow<IFeedTopNavPerRelayFilterSet>,
scope: CoroutineScope,
val newThreads: FeedContentState,
val replies: FeedContentState,
val enabledHomeFeedTypes: StateFlow<Set<HomeFeedType>>,
) : TopNavFeedQueryState(account, listName, followsPerRelay, scope, listOf(newThreads, replies))
class HomeFilterAssembler(
client: INostrClient,
) : TopNavFeedFilterAssembler<HomeQueryState>({ keys -> listOf(HomeOutboxEventsEoseManager(client, keys)) })
@@ -18,7 +18,7 @@
* 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.home.datasource.nip01Core
package com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.nip22Comments.filterHomePostsByScopes
@@ -18,15 +18,15 @@
* 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.home.datasource.nip01Core
package com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomePostsConversationKinds
import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomePostsNewThreadKinds1
import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomePostsNewThreadKinds2
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.nip65Follows.HomePostsConversationKinds
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsNewThreadKinds1
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsNewThreadKinds2
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
fun filterHomePostsByGlobal(
@@ -18,7 +18,7 @@
* 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.home.datasource.nip01Core
package com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.nip22Comments.filterHomePostsByScopes
@@ -18,15 +18,15 @@
* 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.home.datasource.nip01Core
package com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.relay.RelayTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomePostsConversationKinds
import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomePostsNewThreadKinds1
import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomePostsNewThreadKinds2
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.nip65Follows.HomePostsConversationKinds
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsNewThreadKinds1
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsNewThreadKinds2
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
fun filterHomePostsByRelay(
@@ -18,14 +18,14 @@
* 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.home.datasource.nip65Follows
package com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core.filterHomePostsByGeohashes
import com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core.filterHomePostsByHashtags
import com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities.filterHomePostsFromAllCommunities
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.nip72Communities.filterHomePostsFromAllCommunities
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
fun filterHomePostsByAllFollows(
@@ -18,7 +18,7 @@
* 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.home.datasource.nip65Follows
package com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
@@ -18,11 +18,9 @@
* 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.home.datasource.nip65Follows
package com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows
import com.vitorpamplona.amethyst.commons.model.HomeFeedType
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.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavPerRelayFilterSet
@@ -33,39 +31,38 @@ 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.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.home.HomeQueryState
import com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core.filterHomePostsByGeohashes
import com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core.filterHomePostsByGlobal
import com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core.filterHomePostsByHashtags
import com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core.filterHomePostsByRelay
import com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities.filterHomePostsByAllCommunities
import com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities.filterHomePostsByCommunity
import com.vitorpamplona.amethyst.commons.relayClient.home.nip90AlgoFeeds.filterHomePostsByAlgoFeedIds
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
import com.vitorpamplona.amethyst.commons.relayClient.topNavFeeds.TopNavFeedSubAssembler
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByGeohashes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByGlobal
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByHashtags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByRelay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsByAllCommunities
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsByCommunity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip90AlgoFeeds.filterHomePostsByAlgoFeedIds
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.Flow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
/**
* The home feed's REQ builder: dispatches the top-nav selection to the home filters, bounded by the
* new-threads and replies floors, and drops the kinds the user turned off in Settings › Home.
*/
class HomeOutboxEventsEoseManager(
client: INostrClient,
allKeys: () -> Set<HomeQueryState>,
) : PerUserAndFollowListEoseManager<HomeQueryState, TopFilter>(client, allKeys) {
) : TopNavFeedSubAssembler<HomeQueryState>(client, allKeys) {
override fun updateFilter(
key: HomeQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
val feedSettings = key.followsPerRelay()
val newThreadSince = key.feedState.homeNewThreads.lastNoteCreatedAtIfFilled()
val repliesSince = key.feedState.homeReplies.lastNoteCreatedAtIfFilled()
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay.value
val newThreadSince = key.newThreads.lastNoteCreatedAtIfFilled()
val repliesSince = key.replies.lastNoteCreatedAtIfFilled()
val base =
when (feedSettings) {
is AllCommunitiesTopNavPerRelayFilterSet -> filterHomePostsByAllCommunities(feedSettings, since, newThreadSince)
@@ -83,75 +80,14 @@ class HomeOutboxEventsEoseManager(
// Drop the kinds the user turned off in Settings › Home from every home relay filter, so a
// disabled group is never downloaded regardless of which top-nav strategy built the filters.
return base.removeDisabledHomeKinds(HomeFeedType.disabledKinds(key.account.settings.enabledHomeFeedTypes.value))
return base.removeDisabledHomeKinds(HomeFeedType.disabledKinds(key.enabledHomeFeedTypes.value))
}
override fun user(key: HomeQueryState) = key.account.userProfile()
override fun list(key: HomeQueryState) = key.listName()
fun HomeQueryState.listNameFlow() = account.settings.defaultHomeFollowList
fun HomeQueryState.listName() = listNameFlow().value
fun HomeQueryState.followRelayFlow() = account.liveHomeFollowListsPerRelay
fun HomeQueryState.followsPerRelay() = followRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: HomeQueryState): 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.followRelayFlow().sample(1000).collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
// Re-arm the home subscriptions when a content-type toggle flips, so a disabled
// group leaves the live REQ and a re-enabled one comes back without a restart.
key.account.settings.enabledHomeFeedTypes
.drop(1)
.collectLatest { invalidateFilters() }
},
key.account.scope.launch(Dispatchers.IO) {
key.feedState.homeNewThreads.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedState.homeReplies.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() }
}
// Re-arm the home subscriptions when a content-type toggle flips, so a disabled group leaves the
// live REQ and a re-enabled one comes back without a restart.
override fun extraInvalidators(key: HomeQueryState): List<Flow<*>> = listOf(key.enabledHomeFeedTypes.drop(1))
}
/**
* Removes the [disabled] kinds from each home filter. A filter with no `kinds` (e.g. an
* algo-feed id/address fetch) is left untouched; a filter whose kinds all become disabled is
* dropped entirely, since sending it with an empty `kinds` would wrongly match every kind.
*/
private fun List<RelayBasedFilter>.removeDisabledHomeKinds(disabled: Set<Int>): List<RelayBasedFilter> {
if (disabled.isEmpty()) return this
return mapNotNull { relayFilter ->
@@ -18,7 +18,7 @@
* 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.home.datasource.nip72Communities
package com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
@@ -18,7 +18,7 @@
* 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.home.datasource.nip72Communities
package com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
@@ -18,7 +18,7 @@
* 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.home.datasource.nip90AlgoFeeds
package com.vitorpamplona.amethyst.commons.relayClient.home.nip90AlgoFeeds
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavPerRelayFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavPerRelayFilterSet
@@ -18,24 +18,29 @@
* 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
package com.vitorpamplona.amethyst.commons.relayClient.napplets
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.IAccount
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
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.flow.StateFlow
/**
* Keyspace for the connected-apps manifest subscription. Carries the account (for relay selection)
* and the specific authors whose napplet manifests should be fetched — the set of pubkeys that have
* been granted permissions in the user's ledger.
*
* @param homeRelays the user's home (read) relays, where the connected apps' manifests are fetched.
*/
class ConnectedAppsQueryState(
override val account: Account,
override val account: IAccount,
val authors: Set<HexKey>,
val homeRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : AccountScopedQuery
/**
@@ -18,7 +18,7 @@
* 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
package com.vitorpamplona.amethyst.commons.relayClient.napplets
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.napplets.filterNappletsByAuthors
@@ -43,7 +43,7 @@ class ConnectedAppsSubAssembler(
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
if (key.authors.isEmpty()) return emptyList()
val relays = key.account.homeRelays.flow.value
val relays = key.homeRelays.value
if (relays.isEmpty()) return emptyList()
return relays.flatMap { relay ->
filterNappletsByAuthors(relay, key.authors, since?.get(relay)?.time)
@@ -18,14 +18,13 @@
* 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.threadview.datasources.subassembies
package com.vitorpamplona.amethyst.commons.relayClient.thread
import com.vitorpamplona.amethyst.commons.model.ThreadAssembler
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
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
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -40,6 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
* saves the of the thread EOSEs in the long run to avoid re-downloading
*/
class ThreadEventLoaderSubAssembler(
val cache: ICacheProvider,
client: INostrClient,
allKeys: () -> Set<ThreadQueryState>,
) : PerUniqueIdEoseManager<ThreadQueryState, HexKey>(client, allKeys, invalidateAfterEose = true) {
@@ -47,9 +47,9 @@ class ThreadEventLoaderSubAssembler(
key: ThreadQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
val branches = ThreadAssembler(LocalCache).findThreadFor(key.eventId) ?: return null
val defaultRelays = key.account.followPlusAllMineWithSearch.flow.value
return filterMissingEventsForThread(LocalCache, branches, defaultRelays)
val branches = ThreadAssembler(cache).findThreadFor(key.eventId) ?: return null
val defaultRelays = key.defaultRelays.value
return filterMissingEventsForThread(cache, branches, defaultRelays)
}
override fun id(key: ThreadQueryState) = key.eventId
@@ -18,32 +18,41 @@
* 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.threadview.datasources
package com.vitorpamplona.amethyst.commons.relayClient.thread
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.IAccount
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
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.threadview.datasources.subassembies.ThreadEventLoaderSubAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadFilterSubAssembler
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.flow.StateFlow
// This allows multiple screen to be listening to tags, even the same tag
/**
* One screen's interest in a thread. Several screens may open the same thread at once, each with
* its own key.
*
* @param defaultRelays follows' outboxes plus the user's relays and search relays: the fallback set
* asked for a thread event that no relay hint resolves.
*/
@Stable
class ThreadQueryState(
val eventId: HexKey,
override val account: Account,
override val account: IAccount,
val defaultRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : AccountScopedQuery
@Stable
class ThreadFilterAssembler(
cache: ICacheProvider,
client: INostrClient,
) : ComposeSubscriptionManager<ThreadQueryState>() {
val group =
listOf(
ThreadFilterSubAssembler(client, ::allKeys),
ThreadEventLoaderSubAssembler(client, ::allKeys),
ThreadFilterSubAssembler(cache, client, ::allKeys),
ThreadEventLoaderSubAssembler(cache, client, ::allKeys),
)
override fun invalidateKeys() = invalidateFilters()
@@ -18,14 +18,13 @@
* 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.threadview.datasources.subassembies
package com.vitorpamplona.amethyst.commons.relayClient.thread
import com.vitorpamplona.amethyst.commons.model.ThreadAssembler
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
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
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -37,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
* saves the of the thread EOSEs in the long run to avoid re-downloading
*/
class ThreadFilterSubAssembler(
val cache: ICacheProvider,
client: INostrClient,
allKeys: () -> Set<ThreadQueryState>,
) : PerUniqueIdEoseManager<ThreadQueryState, HexKey>(client, allKeys) {
@@ -44,7 +44,7 @@ class ThreadFilterSubAssembler(
key: ThreadQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
val root = ThreadAssembler(LocalCache).findRoot(key.eventId) ?: return null
val root = ThreadAssembler(cache).findRoot(key.eventId) ?: return null
return filterEventsInThreadForRoot(root, since)
}
@@ -18,17 +18,17 @@
* 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.home.datasource.nip90AlgoFeeds
package com.vitorpamplona.amethyst.commons.relayClient.home.nip90AlgoFeeds
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavPerRelayFilter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavPerRelayFilterSet
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class FilterHomePostsByAlgoFeedIdsTest {
private val userRelay = RelayUrlNormalizer.normalizeOrNull("wss://user.example/")!!