mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat(relays): explain discovery filters by the feed selection behind them
Commit4f1bd6e0c2left the six public-chat discovery producers untagged and justified it as "they search for chats rather than serving known ones, so there is no entity to name". The first half is right and the conclusion does not follow: having no entity is not the same as having no explanation. Every one of those filters is built from a top-nav selection — Global, your follows, a hashtag, a geohash, a community — which was known where the filter was built and simply had nowhere to travel. The screen could only render them as "All", which is the one thing they are not.3f4723437ealready moved the 25 top-nav value types into commons for exactly this, so ExplainedFilter now carries the scope. It carries the per-relay value rather than the whole set: the filter is already scoped to one relay, so it holds only the slice that applies to it and no reference to the other relays' authors. It stays a typed value rather than a formatted string because purposeDetail already taught that lesson — text built in commons can never be translated, so the UI matches on the type and picks its own wording. scopedTo() stamps it at each feed's make…Filter dispatch, the last place that still knows the selection; below it the builders have flattened it into authors/#t/#g and it is unrecoverable. IFeedTopNavPerRelayFilterSet grew scopeFor(relay) so that stamping is compiler-enforced across all 11 sets rather than a type-switch that silently misses the next one added. The screen groups these rows by scope *type*, not contents: an author-based selection sends a different slice of the follow list to every relay, so keying on contents would shatter "People you follow" into one row per relay — the opposite of what the screen is for. ExplainedFilterTest had not compiled since4d53bbea9erenamed entityId to entityIds, because `./gradlew test` does not run :commons:jvmTest. Repaired, and extended to pin the new field: the scope is a slice of the user's follow list or their chosen hashtag, and handing a relay the selection rather than the authors it already sees would tell it which of its neighbours' filters belong together. Verified on emulator-5554: Home Feed's row now reads "People you follow" with its 176 relays, as one row rather than 176. The public-chat discovery producers take the identical path but only mount while the Discover→Chats screen is open, which this device's bottom nav has no tab for, so that specific row is unproven on device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
548a5c979e
commit
8e019a2d59
+2
-1
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
@@ -47,7 +48,7 @@ fun makeBadgesFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterBadgesByMutedAuthors(feedSettings, since, defaultSince)
|
||||
is GlobalTopNavPerRelayFilterSet -> filterBadgesGlobal(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
private fun filterBadgesByAuthorsOnRelay(
|
||||
relay: NormalizedRelayUrl,
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavP
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.subassemblies.filterCalendarsByAuthors
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.subassemblies.filterCalendarsByFollows
|
||||
@@ -49,4 +50,4 @@ fun makeCalendarsFilter(
|
||||
is LocationTopNavPerRelayFilterSet -> filterCalendarsByGeohashes(feedSettings, since, defaultSince)
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterCalendarsByMutedAuthors(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ 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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.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
|
||||
@@ -58,4 +59,4 @@ fun filterRelayGroupsDiscovery(
|
||||
is RelayTopNavPerRelayFilterSet -> filterRelayGroupsByRelay(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterRelayGroupsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+4
-1
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay
|
||||
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -88,7 +89,9 @@ class RelayGroupsDiscoverySubAssembler(
|
||||
)
|
||||
}
|
||||
|
||||
return base + extra
|
||||
// `base` is scoped by its own builder; these host-relay rosters are built here, so they get
|
||||
// the same selection stamped on them rather than showing up as an unexplained extra.
|
||||
return base + extra.scopedTo(feedSettings)
|
||||
}
|
||||
|
||||
/** Relays that host NIP-29 groups the user is connected to: joined (kind-10009) + favorited (kind-10012). */
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByAuthors
|
||||
@@ -54,4 +55,4 @@ fun makeLongFormFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterLongFormByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByAuthors
|
||||
@@ -54,4 +55,4 @@ fun makePublicChatsFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterPublicChatsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByAuthors
|
||||
@@ -54,4 +55,4 @@ fun makeFollowSetsFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterFollowSetsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByAuthors
|
||||
@@ -54,4 +55,4 @@ fun makeLiveActivitiesFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterLiveActivitiesByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByAuthors
|
||||
@@ -54,4 +55,4 @@ fun makeCommunitiesFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterCommunitiesByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByAuthors
|
||||
@@ -54,4 +55,4 @@ fun makeContentDVMsFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterContentDVMsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByAuthors
|
||||
@@ -54,4 +55,4 @@ fun makeClassifiedsFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterClassifiedsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavP
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasource.subassemblies.filterBrowseEmojiSetsByAuthors
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasource.subassemblies.filterBrowseEmojiSetsByFollows
|
||||
@@ -46,4 +47,4 @@ fun makeBrowseEmojiSetsFilter(
|
||||
is GlobalTopNavPerRelayFilterSet -> filterBrowseEmojiSetsGlobal(feedSettings, since, defaultSince)
|
||||
is HashtagTopNavPerRelayFilterSet -> filterBrowseEmojiSetsByHashtag(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByAuthors
|
||||
@@ -55,4 +56,4 @@ fun makeGitRepositoriesFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterGitRepositoriesByMutedAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterGitRepositoriesByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavP
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.datasource.subassemblies.filterHighlightsByAuthors
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.datasource.subassemblies.filterHighlightsByFollows
|
||||
@@ -57,4 +58,4 @@ fun makeHighlightsFilter(
|
||||
is LocationTopNavPerRelayFilterSet -> filterHighlightsByGeohashes(feedSettings, since, defaultSince)
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterHighlightsByMutedAuthors(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ 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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.model.HomeFeedType
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -78,7 +79,7 @@ class HomeOutboxEventsEoseManager(
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterHomePostsByCommunity(feedSettings, since, newThreadSince)
|
||||
is FavoriteAlgoFeedTopNavPerRelayFilterSet -> filterHomePostsByAlgoFeedIds(feedSettings, since, newThreadSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
// 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.
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.subassemblies.filterLongsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.subassemblies.filterLongsByAuthors
|
||||
@@ -55,4 +56,4 @@ fun makeLongsFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterLongsByMutedAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterLongsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.MUSIC_PLAYLIST_KINDS
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.subassemblies.MUSIC_TRACK_KINDS
|
||||
@@ -82,4 +83,4 @@ private fun makeMusicFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterMusicEventsByMutedAuthors(feedSettings, kinds, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterMusicEventsByCommunity(feedSettings, kinds, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollow
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.subassemblies.filterNappletsByAuthors
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.subassemblies.filterNappletsByFollows
|
||||
@@ -51,4 +52,4 @@ fun makeNappletsFilter(
|
||||
is GlobalTopNavPerRelayFilterSet -> filterNappletsGlobal(feedSettings, since, defaultSince)
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterNappletsByMutedAuthors(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.subassemblies.filterNestsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.subassemblies.filterNestsByAuthors
|
||||
@@ -56,7 +57,7 @@ fun makeNestsFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterNestsByAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterNestsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> return emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
if (rooms.isEmpty()) return rooms
|
||||
|
||||
// Add a single presence probe per relay we're already querying for
|
||||
|
||||
+2
-1
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollow
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource.subassemblies.filterNsitesByAuthors
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nsites.datasource.subassemblies.filterNsitesByFollows
|
||||
@@ -51,4 +52,4 @@ fun makeNsitesFilter(
|
||||
is GlobalTopNavPerRelayFilterSet -> filterNsitesGlobal(feedSettings, since, defaultSince)
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterNsitesByMutedAuthors(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.subassemblies.filterPicturesByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.subassemblies.filterPicturesByAuthors
|
||||
@@ -55,4 +56,4 @@ fun makePicturesFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterPicturesByMutedAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterPicturesByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.subassemblies.PODCASTING20_METADATA_KINDS
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.subassemblies.PODCAST_EPISODE_KINDS
|
||||
@@ -78,4 +79,4 @@ private fun makePodcastFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterPodcastEventsByMutedAuthors(feedSettings, kinds, since, defaultSince, additionalTags)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterPodcastEventsByCommunity(feedSettings, kinds, since, defaultSince, additionalTags)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies.filterPollsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies.filterPollsByAuthors
|
||||
@@ -55,4 +56,4 @@ fun makePollsFilter(
|
||||
is LocationTopNavPerRelayFilterSet -> filterPollsByGeohashes(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterPollsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+38
-1
@@ -60,6 +60,15 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurposeGroup
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -284,6 +293,29 @@ private fun PurposeCard(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The feed selection a discovery filter is searching within, in the app's own words.
|
||||
*
|
||||
* Hashtags and geohashes read out their own values — they are short, and the whole point is *which*
|
||||
* hashtag. The author-based selections do not: a follow list is thousands of keys, and per relay it
|
||||
* is a different slice of them, so naming the kind is the honest summary.
|
||||
*/
|
||||
@Composable
|
||||
private fun scopeLabel(scope: IFeedTopNavPerRelayFilter): String? =
|
||||
when (scope) {
|
||||
is GlobalTopNavPerRelayFilter -> stringRes(R.string.active_subs_scope_global)
|
||||
is AllFollowsTopNavPerRelayFilter -> stringRes(R.string.active_subs_scope_follows)
|
||||
is AuthorsTopNavPerRelayFilter -> stringRes(R.string.active_subs_scope_authors)
|
||||
is MutedAuthorsTopNavPerRelayFilter -> stringRes(R.string.active_subs_scope_muted)
|
||||
is AllCommunitiesTopNavPerRelayFilter -> stringRes(R.string.active_subs_scope_all_communities)
|
||||
is FavoriteAlgoFeedTopNavPerRelayFilter -> stringRes(R.string.active_subs_scope_algo)
|
||||
is HashtagTopNavPerRelayFilter -> scope.hashtags.sorted().joinToString(", ") { "#$it" }
|
||||
is LocationTopNavPerRelayFilter -> scope.geotags.sorted().joinToString(", ")
|
||||
// The community and the relay already name themselves — the community through its own
|
||||
// entity row, the relay through the row's relay list — so repeating it here would be noise.
|
||||
else -> null
|
||||
}
|
||||
|
||||
/** The entity's name, coloured as a link only when tapping it actually goes somewhere. */
|
||||
@Composable
|
||||
private fun EntityLabelText(
|
||||
@@ -400,7 +432,12 @@ private fun EntityBlock(
|
||||
// Deliberately not falling back to `entity.detail`: that field is a developer breadcrumb set in
|
||||
// `commons`, where Android string resources do not exist, so it is hardcoded English and could
|
||||
// never be translated. A localized "no entity" line is better than an untranslatable one.
|
||||
val label = resolved?.name ?: stringRes(R.string.active_subs_no_entity)
|
||||
//
|
||||
// A discovery filter has no entity by nature — it searches for chats and articles rather than
|
||||
// serving ones already named — but it does carry the selection it searches within, which is a
|
||||
// far better answer than "no entity". The scope arrives as a typed value for exactly this
|
||||
// reason: the wording is chosen here, where translations exist.
|
||||
val label = resolved?.name ?: entity.scope?.let { scopeLabel(it) } ?: stringRes(R.string.active_subs_no_entity)
|
||||
|
||||
Column {
|
||||
Row(
|
||||
|
||||
+34
-7
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -56,11 +57,29 @@ import kotlinx.coroutines.withContext
|
||||
data class SubscriptionEntityRow(
|
||||
/** Null when the filter named no entity — "the rest of this purpose", not a real entity. */
|
||||
val entityId: HexKey?,
|
||||
/**
|
||||
* The top-nav selection behind a filter that names no entity — what a discovery filter is
|
||||
* searching *within*. Null for filters that name a real entity, which speaks for itself.
|
||||
*/
|
||||
val scope: IFeedTopNavPerRelayFilter?,
|
||||
val detail: String?,
|
||||
val relays: List<NormalizedRelayUrl>,
|
||||
val filterCount: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* What makes two filters the same row.
|
||||
*
|
||||
* [scopeKey] is the scope's **type**, not its contents: an author-based selection carries a
|
||||
* different slice of the follow list to every relay, so keying on contents would shatter "People you
|
||||
* follow" into one row per relay — the opposite of what this screen is for. Selections whose
|
||||
* contents are the same everywhere (a hashtag, a geohash) render theirs from the retained instance.
|
||||
*/
|
||||
private data class EntityKey(
|
||||
val entityId: HexKey?,
|
||||
val scopeKey: String?,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class SubscriptionPurposeRow(
|
||||
val purpose: SubPurpose,
|
||||
@@ -109,8 +128,9 @@ class ActiveSubscriptionsViewModel : ViewModel() {
|
||||
val client = Amethyst.instance.client
|
||||
|
||||
// account -> purpose -> entity -> relays / count
|
||||
val byAccount = mutableMapOf<HexKey?, MutableMap<SubPurpose, MutableMap<HexKey?, MutableList<NormalizedRelayUrl>>>>()
|
||||
val detailOf = mutableMapOf<Pair<SubPurpose, HexKey?>, String?>()
|
||||
val byAccount = mutableMapOf<HexKey?, MutableMap<SubPurpose, MutableMap<EntityKey, MutableList<NormalizedRelayUrl>>>>()
|
||||
val detailOf = mutableMapOf<Pair<SubPurpose, EntityKey>, String?>()
|
||||
val scopeOf = mutableMapOf<Pair<SubPurpose, EntityKey>, IFeedTopNavPerRelayFilter>()
|
||||
var total = 0
|
||||
var untagged = 0
|
||||
val allRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
@@ -124,17 +144,23 @@ class ActiveSubscriptionsViewModel : ViewModel() {
|
||||
return@forEach
|
||||
}
|
||||
allRelays.add(relay)
|
||||
// Discovery filters name no entity — they go looking for things rather than
|
||||
// serving known ones — but they do carry the selection they search within, which
|
||||
// is what keeps them from collapsing into one nameless row per purpose.
|
||||
val scopeKey = explained.scope?.let { it::class.simpleName }
|
||||
// A batched filter serves several entities at once — relay-group state is one #d
|
||||
// filter per host relay carrying every joined group on it — so it contributes a
|
||||
// row to each of them rather than collapsing to "All".
|
||||
val entities: List<HexKey?> = explained.entityIds?.takeIf { it.isNotEmpty() } ?: listOf(null)
|
||||
entities.forEach { entityId ->
|
||||
val key = EntityKey(entityId, scopeKey)
|
||||
byAccount
|
||||
.getOrPut(explained.accountPubKey) { mutableMapOf() }
|
||||
.getOrPut(explained.purpose) { mutableMapOf() }
|
||||
.getOrPut(entityId) { mutableListOf() }
|
||||
.getOrPut(key) { mutableListOf() }
|
||||
.add(relay)
|
||||
detailOf[explained.purpose to entityId] = explained.purposeDetail
|
||||
detailOf[explained.purpose to key] = explained.purposeDetail
|
||||
explained.scope?.let { scopeOf.getOrPut(explained.purpose to key) { it } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,10 +173,11 @@ class ActiveSubscriptionsViewModel : ViewModel() {
|
||||
.map { (purpose, entities) ->
|
||||
val entityRows =
|
||||
entities
|
||||
.map { (entityId, relays) ->
|
||||
.map { (key, relays) ->
|
||||
SubscriptionEntityRow(
|
||||
entityId = entityId,
|
||||
detail = detailOf[purpose to entityId],
|
||||
entityId = key.entityId,
|
||||
scope = scopeOf[purpose to key],
|
||||
detail = detailOf[purpose to key],
|
||||
relays = relays.distinct().sortedBy { it.url },
|
||||
filterCount = relays.size,
|
||||
)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.subassemblies.filterShortsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.subassemblies.filterShortsByAuthors
|
||||
@@ -55,4 +56,4 @@ fun makeShortsFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterShortsByMutedAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterShortsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavP
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource.subassemblies.filterSoftwareAppsByAuthors
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource.subassemblies.filterSoftwareAppsByFollows
|
||||
@@ -48,4 +49,4 @@ fun makeSoftwareAppsFilter(
|
||||
is HashtagTopNavPerRelayFilterSet -> filterSoftwareAppsByHashtag(feedSettings, since, defaultSince)
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterSoftwareAppsByMutedAuthors(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.allcommuni
|
||||
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.subscriptions.scopedTo
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.datasource.subassemblies.filterWorkoutsByAllCommunities
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.datasource.subassemblies.filterWorkoutsByAuthors
|
||||
@@ -55,4 +56,4 @@ fun makeWorkoutsFilter(
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterWorkoutsByMutedAuthors(feedSettings, since, defaultSince)
|
||||
is SingleCommunityTopNavPerRelayFilterSet -> filterWorkoutsByCommunity(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.scopedTo(feedSettings)
|
||||
|
||||
@@ -2151,6 +2151,12 @@
|
||||
<string name="active_subs_pair">%1$s \u00b7 %2$s</string>
|
||||
<string name="active_subs_unattributed">Not attributed to an account</string>
|
||||
<string name="active_subs_no_entity">All</string>
|
||||
<string name="active_subs_scope_global">Everyone</string>
|
||||
<string name="active_subs_scope_follows">People you follow</string>
|
||||
<string name="active_subs_scope_authors">A chosen list of people</string>
|
||||
<string name="active_subs_scope_muted">Muted people</string>
|
||||
<string name="active_subs_scope_all_communities">Your communities</string>
|
||||
<string name="active_subs_scope_algo">A favorite algo feed</string>
|
||||
<string name="active_subs_share">%1$d%% of all</string>
|
||||
<string name="active_subs_search_keywords">subscriptions filters relays requests reqs connections why diagnostics</string>
|
||||
<string name="relay_explain_home">Posts by people you follow, read from the relays each of them publishes to.</string>
|
||||
|
||||
+16
-1
@@ -20,4 +20,19 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.topNavFeeds
|
||||
|
||||
interface IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
interface IFeedTopNavPerRelayFilterSet {
|
||||
/**
|
||||
* What this feed selection asks of one relay — the follows, hashtags, geohashes or communities
|
||||
* scoped to it — or null when the set says nothing per relay.
|
||||
*
|
||||
* Declared here so an `ExplainedFilter` can carry the scope that produced it. A discovery filter
|
||||
* names no entity (it searches for things rather than serving known ones), which used to leave
|
||||
* the Active Subscriptions screen with a nameless row it could only label "no entity". The
|
||||
* selection behind it was always known at build time; it just had nowhere to travel. Returning
|
||||
* the per-relay value rather than the whole set keeps it exact: the filter is already per relay,
|
||||
* so it carries only the slice that applies to it.
|
||||
*/
|
||||
fun scopeFor(relay: NormalizedRelayUrl): IFeedTopNavPerRelayFilter?
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class AllFollowsTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, AllFollowsTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = set[relay]
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class LocationTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, LocationTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = set[relay]
|
||||
}
|
||||
|
||||
+5
-1
@@ -39,4 +39,8 @@ class FavoriteAlgoFeedTopNavPerRelayFilterSet(
|
||||
val contentFetches: Map<NormalizedRelayUrl, FavoriteAlgoFeedTopNavPerRelayFilter>,
|
||||
val listenRelays: Set<NormalizedRelayUrl>,
|
||||
val requestIds: Set<HexKey>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
// Only the content half is per-relay. [listenRelays] is where the DVMs answer, which is a
|
||||
// delivery address rather than a scope, so a filter aimed there carries none.
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = contentFetches[relay]
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class GlobalTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, GlobalTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = set[relay]
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class HashtagTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, HashtagTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = set[relay]
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class AllCommunitiesTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, AllCommunitiesTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = set[relay]
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class AuthorsTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, AuthorsTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = set[relay]
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class SingleCommunityTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, SingleCommunityTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = set[relay]
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class MutedAuthorsTopNavPerRelayFilterSet(
|
||||
val set: Map<NormalizedRelayUrl, MutedAuthorsTopNavPerRelayFilter>,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl) = set[relay]
|
||||
}
|
||||
|
||||
+6
-1
@@ -20,9 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.topNavFeeds.relay
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class RelayTopNavPerRelayFilterSet(
|
||||
val relayUrl: NormalizedRelayUrl,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
) : IFeedTopNavPerRelayFilterSet {
|
||||
// The relay *is* the whole selection here, so there is nothing per-relay left to say — the
|
||||
// filter's own relay already carries it.
|
||||
override fun scopeFor(relay: NormalizedRelayUrl): IFeedTopNavPerRelayFilter? = null
|
||||
}
|
||||
|
||||
+5
-1
@@ -20,6 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.topNavFeeds.unknown
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
object UnknownTopNavPerRelayFilterSet : IFeedTopNavPerRelayFilterSet
|
||||
object UnknownTopNavPerRelayFilterSet : IFeedTopNavPerRelayFilterSet {
|
||||
override fun scopeFor(relay: NormalizedRelayUrl): IFeedTopNavPerRelayFilter? = null
|
||||
}
|
||||
|
||||
+48
-2
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.relayClient.subscriptions
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilter
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
@@ -89,6 +91,24 @@ class ExplainedFilter(
|
||||
* account alive.
|
||||
*/
|
||||
val accountPubKey: HexKey? = null,
|
||||
/**
|
||||
* The top-nav selection that produced this filter — Global, the user's follows, a hashtag, a
|
||||
* geohash, a community.
|
||||
*
|
||||
* [entityIds] answers "which known thing does this serve"; discovery filters have no such thing,
|
||||
* because they go looking for chats/articles/streams rather than serving ones already named. That
|
||||
* is not the same as having no explanation: the feed selection behind them was always known where
|
||||
* the filter was built, it simply had nowhere to travel, so the screen could only render those
|
||||
* rows as "no entity".
|
||||
*
|
||||
* The per-relay value, not the whole set: this filter is already scoped to one relay, so it
|
||||
* carries only the slice that applies to it and holds no reference to the other relays' authors.
|
||||
*
|
||||
* A typed value rather than a formatted string, because [purposeDetail] taught the lesson —
|
||||
* text built in `commons` can never be translated. The UI matches on the type and picks its own
|
||||
* localized wording.
|
||||
*/
|
||||
val scope: IFeedTopNavPerRelayFilter? = null,
|
||||
) : Filter(ids, authors, kinds, tags, tagsAll, since, until, limit, search) {
|
||||
override fun copy(
|
||||
ids: List<String>?,
|
||||
@@ -100,7 +120,7 @@ class ExplainedFilter(
|
||||
until: Long?,
|
||||
limit: Int?,
|
||||
search: String?,
|
||||
) = ExplainedFilter(ids, authors, kinds, tags, tagsAll, since, until, limit, search, purpose, purposeDetail, entityIds, accountPubKey)
|
||||
) = ExplainedFilter(ids, authors, kinds, tags, tagsAll, since, until, limit, search, purpose, purposeDetail, entityIds, accountPubKey, scope)
|
||||
|
||||
companion object {
|
||||
/** Tags [filter] with a [purpose], preserving every protocol field. */
|
||||
@@ -110,6 +130,7 @@ class ExplainedFilter(
|
||||
detail: String? = null,
|
||||
entityIds: List<HexKey>? = null,
|
||||
accountPubKey: HexKey? = null,
|
||||
scope: IFeedTopNavPerRelayFilter? = null,
|
||||
) = ExplainedFilter(
|
||||
filter.ids,
|
||||
filter.authors,
|
||||
@@ -124,6 +145,7 @@ class ExplainedFilter(
|
||||
detail,
|
||||
entityIds,
|
||||
accountPubKey,
|
||||
scope,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -173,7 +195,31 @@ fun List<RelayBasedFilter>.attributedTo(accountPubKey: HexKey): List<RelayBasedF
|
||||
if (filter is ExplainedFilter && filter.accountPubKey == null) {
|
||||
RelayBasedFilter(
|
||||
relay = relayFilter.relay,
|
||||
filter = ExplainedFilter.of(filter, filter.purpose, filter.purposeDetail, filter.entityIds, accountPubKey),
|
||||
filter = ExplainedFilter.of(filter, filter.purpose, filter.purposeDetail, filter.entityIds, accountPubKey, filter.scope),
|
||||
)
|
||||
} else {
|
||||
relayFilter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamps the top-nav selection onto every tagged filter that does not already name one.
|
||||
*
|
||||
* Applied once at each feed's `make…Filter` dispatch — the single place that still knows which
|
||||
* selection is being served, and the same place that already chooses a builder from it. Below that
|
||||
* point the builders have flattened it into `authors`/`#t`/`#g` and the selection is unrecoverable.
|
||||
*
|
||||
* Each filter gets the slice for its own relay, so a filter aimed at a relay the selection says
|
||||
* nothing about is simply left alone rather than labelled with someone else's scope.
|
||||
*/
|
||||
fun List<RelayBasedFilter>.scopedTo(feedSettings: IFeedTopNavPerRelayFilterSet): List<RelayBasedFilter> =
|
||||
map { relayFilter ->
|
||||
val filter = relayFilter.filter
|
||||
val scope = if (filter is ExplainedFilter && filter.scope == null) feedSettings.scopeFor(relayFilter.relay) else null
|
||||
if (filter is ExplainedFilter && scope != null) {
|
||||
RelayBasedFilter(
|
||||
relay = relayFilter.relay,
|
||||
filter = ExplainedFilter.of(filter, filter.purpose, filter.purposeDetail, filter.entityIds, filter.accountPubKey, scope),
|
||||
)
|
||||
} else {
|
||||
relayFilter
|
||||
|
||||
+11
-3
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.relayClient.subscriptions
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.FiltersChanged
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -56,8 +57,9 @@ class ExplainedFilterTest {
|
||||
limit = 20,
|
||||
purpose = SubPurpose.NOTIFICATIONS,
|
||||
purposeDetail = "inbox relays for the active account",
|
||||
entityId = "cafe0000000000000000000000000000000000000000000000000000000000ff",
|
||||
entityIds = listOf("cafe0000000000000000000000000000000000000000000000000000000000ff"),
|
||||
accountPubKey = pubkey,
|
||||
scope = HashtagTopNavPerRelayFilter(setOf("askednostr")),
|
||||
)
|
||||
|
||||
// ---- the wire must not learn why we asked -------------------------------
|
||||
@@ -81,6 +83,11 @@ class ExplainedFilterTest {
|
||||
assertFalse("detail leaked to the wire: $json", json.contains("inbox relays", ignoreCase = true))
|
||||
assertFalse("entityId leaked to the wire: $json", json.contains("cafe0000", ignoreCase = true))
|
||||
assertFalse("accountPubKey leaked as a field: $json", json.contains("accountPubKey", ignoreCase = true))
|
||||
// The scope is the feed selection behind the filter — a hashtag here, but for a follows feed
|
||||
// it is a slice of the user's follow list. Handing a relay the *selection* rather than the
|
||||
// authors it already sees would tell it which of its neighbours' filters belong together.
|
||||
assertFalse("scope leaked to the wire: $json", json.contains("askednostr", ignoreCase = true))
|
||||
assertFalse("scope leaked as a field: $json", json.contains("scope", ignoreCase = true))
|
||||
}
|
||||
|
||||
/** The filter is serialized as part of a REQ, so check the real command too, not just the filter. */
|
||||
@@ -106,9 +113,10 @@ class ExplainedFilterTest {
|
||||
assertTrue("copy() must stay an ExplainedFilter", advanced is ExplainedFilter)
|
||||
assertEquals(SubPurpose.NOTIFICATIONS, advanced.purposeOrNull())
|
||||
assertEquals("inbox relays for the active account", (advanced as ExplainedFilter).purposeDetail)
|
||||
// entityId/accountPubKey ride the same path and would vanish just as silently
|
||||
assertEquals("cafe0000000000000000000000000000000000000000000000000000000000ff", advanced.entityId)
|
||||
// entityIds/accountPubKey/scope ride the same path and would vanish just as silently
|
||||
assertEquals(listOf("cafe0000000000000000000000000000000000000000000000000000000000ff"), advanced.entityIds)
|
||||
assertEquals(pubkey, advanced.accountPubKey)
|
||||
assertEquals(setOf("askednostr"), (advanced.scope as? HashtagTopNavPerRelayFilter)?.hashtags)
|
||||
assertEquals(1_785_379_272L, advanced.since)
|
||||
// and the protocol fields came along untouched
|
||||
assertEquals(listOf(pubkey), advanced.authors)
|
||||
|
||||
Reference in New Issue
Block a user